From 8d10cc4dce815eb998e6a4155a372886580e1fb8 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Tue, 8 Sep 2026 10:16:17 -0400 Subject: [PATCH] Remove calls from unread reminders. --- .../app/internal/InternalSettingsFragment.kt | 4 +- .../app/notifications/ReminderType.kt | 1 - .../securesms/database/CallTable.kt | 69 ------ .../securesms/database/MessageTable.kt | 1 - .../securesms/database/ThreadTable.kt | 30 +-- .../securesms/jobs/UnreadReminderJob.kt | 40 +--- .../service/UnreadReminderManager.kt | 14 +- app/src/main/res/values/strings.xml | 15 +- .../CallTableTest_unreadForReminder.kt | 115 ---------- .../ThreadTableTest_mutedThreadIds.kt | 54 ++--- .../securesms/jobs/UnreadReminderJobTest.kt | 200 +++++------------- 11 files changed, 89 insertions(+), 454 deletions(-) delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/database/CallTableTest_unreadForReminder.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 58b5d3cea0..5749e89378 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -42,7 +42,6 @@ import org.thoughtcrime.securesms.calls.quality.CallQualityBottomSheetFragment import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText -import org.thoughtcrime.securesms.components.settings.app.notifications.ReminderType import org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.configure @@ -440,8 +439,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter title = DSLSettingsText.from("Run unread reminder job"), summary = DSLSettingsText.from("Generates an unread reminder notification based on unreads and muted preferences. Skips the three day cooldown."), onClick = { - val threadIds = (SignalDatabase.threads.getMutedThreadIds(ReminderType.MESSAGES, 0) + SignalDatabase.threads.getMutedThreadIds(ReminderType.CALLS, 0)).distinct() - threadIds.forEach { threadId -> UnreadReminderJob.enqueue(threadId, 0) } + SignalDatabase.threads.getMutedThreadIds(0).forEach { threadId -> UnreadReminderJob.enqueue(threadId, 0) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/ReminderType.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/ReminderType.kt index 5f2b03a6c2..45cb211bec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/ReminderType.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/ReminderType.kt @@ -10,7 +10,6 @@ package org.thoughtcrime.securesms.components.settings.app.notifications */ enum class ReminderType { MESSAGES, - CALLS, MENTIONS, REPLIES } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt index 6902cb5cc8..d8ee10d25f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/CallTable.kt @@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob import org.thoughtcrime.securesms.jobs.CallSyncEventJob import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.service.UnreadReminderManager import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId import org.whispersystems.signalservice.internal.push.SyncMessage.CallEvent import java.util.UUID @@ -194,74 +193,6 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl .readToSingleLong() } - /** - * Returns the number of unread calls and up to three distinct callers from [threadId]. - */ - fun getUnreadCallsForReminderNotification(threadId: Long, now: Long = System.currentTimeMillis()): Pair> { - val peerIds = readableDatabase - .select("$TABLE_NAME.$PEER") - .from("$TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} ON $TABLE_NAME.$MESSAGE_ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID}") - .where( - """ - ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = $threadId AND - $TABLE_NAME.$READ = ${ReadState.serialize(ReadState.UNREAD)} AND - ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND - $TABLE_NAME.$TIMESTAMP > ${now - UnreadReminderManager.MAX_UNREAD_MESSAGE_AGE.inWholeMilliseconds} - """ - ) - .orderBy("$TABLE_NAME.$TIMESTAMP DESC") - .run() - .readToList { cursor -> RecipientId.from(cursor.requireLong(PEER)) } - - return peerIds.size to peerIds.distinct().take(3) - } - - /** - * Returns true if [threadId] has an unread missed call that occurred after [since]. - */ - fun hasUnreadCallsSince(threadId: Long, since: Long): Boolean { - return readableDatabase - .exists("$TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} ON $TABLE_NAME.$MESSAGE_ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID}") - .where( - """ - ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = $threadId AND - $TABLE_NAME.$READ = ${ReadState.serialize(ReadState.UNREAD)} AND - ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND - $TABLE_NAME.$TIMESTAMP > $since - """ - ) - .run() - } - - /** - * Returns the thread id and timestamp of the oldest unread missed call across [threadIds]. - * If there is none, it returns -1 for thread id and Long.MAX_VALUE for timestamp. - */ - fun getOldestUnreadCall(threadIds: List): Pair { - if (threadIds.isEmpty()) { - return Pair(-1, Long.MAX_VALUE) - } - - val query = SqlUtil.buildFastCollectionQuery("${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}", threadIds) - - return readableDatabase - .select("${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}", "$TABLE_NAME.$TIMESTAMP") - .from("$TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} ON $TABLE_NAME.$MESSAGE_ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID}") - .where( - """ - ${query.where} AND - $TABLE_NAME.$READ = ${ReadState.serialize(ReadState.UNREAD)} AND - ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND - $TABLE_NAME.$TIMESTAMP > ${System.currentTimeMillis() - UnreadReminderManager.MAX_UNREAD_MESSAGE_AGE.inWholeMilliseconds} - """, - query.whereArgs - ) - .orderBy("$TABLE_NAME.$TIMESTAMP ASC") - .limit(1) - .run() - .readToSingleObject { cursor -> cursor.requireLong(MessageTable.THREAD_ID) to cursor.requireLong(TIMESTAMP) } ?: Pair(-1, Long.MAX_VALUE) - } - fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event, fromSync: Boolean = false) { val messageType: Long = Call.getMessageType(type, direction, event) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index a892244f37..0210555a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -5403,7 +5403,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat /** * Returns the number of unread messages (based on the [ReminderType]) and up to three distinct authors from [threadId] - * For missed calls, see [getUnreadContentForReminderNotification] in the calls table. */ fun getUnreadContentForReminderNotification(threadId: Long, type: ReminderType, now: Long = System.currentTimeMillis()): Pair> { val categoryClause = when (type) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 1a0628c4a2..97245db64c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -41,7 +41,6 @@ import org.signal.core.util.withinTransaction import org.signal.libsignal.zkgroup.InvalidInputException import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord -import org.thoughtcrime.securesms.components.settings.app.notifications.ReminderType import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments @@ -679,17 +678,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa } /** - * Gets eligible threads that could quality for unread reminders depending on its while muted settings. - * e.g. when getting missed calls, we get the muted threads that have opted into unread reminders - * and also have notify for calls while muted on. + * Gets eligible threads that could quality for unread reminders (opted in, has unread messages, elapsed time). */ - fun getMutedThreadIds(reminderType: ReminderType, reminderThreshold: Long, now: Long = System.currentTimeMillis()): List { - val unreadReminderClause = getNotificationClause(RecipientTable.UNREAD_REMINDER, SignalStore.settings.unreadReminderEnabled) - - val reminderClause = when (reminderType) { - ReminderType.MESSAGES -> "AND $unreadReminderClause AND $UNREAD_COUNT > 0" - ReminderType.CALLS -> "AND $unreadReminderClause AND (${getNotificationClause(RecipientTable.CALL_NOTIFICATION_SETTING, SignalStore.settings.allowCallsWhileMuted)})" - else -> "" + fun getMutedThreadIds(reminderThreshold: Long, now: Long = System.currentTimeMillis()): List { + val unreadReminderClause = if (SignalStore.settings.unreadReminderEnabled) { + "${RecipientTable.TABLE_NAME}.${RecipientTable.UNREAD_REMINDER} != ${RecipientTable.NotificationSetting.DO_NOT_NOTIFY.id}" + } else { + "${RecipientTable.TABLE_NAME}.${RecipientTable.UNREAD_REMINDER} = ${RecipientTable.NotificationSetting.ALWAYS_NOTIFY.id}" } return readableDatabase @@ -699,9 +694,10 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa """ $ACTIVE = 1 AND $ARCHIVED = 0 AND + $UNREAD_COUNT > 0 AND $LAST_UNREAD_REMINDER < ${now - reminderThreshold} AND - ${RecipientTable.MUTE_UNTIL} >= $now - $reminderClause + ${RecipientTable.MUTE_UNTIL} >= $now AND + $unreadReminderClause """.trimIndent() ) .run() @@ -725,14 +721,6 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa .run() } - private fun getNotificationClause(column: String, allowByDefault: Boolean): String { - return if (allowByDefault) { - "${RecipientTable.TABLE_NAME}.$column != ${RecipientTable.NotificationSetting.DO_NOT_NOTIFY.id}" - } else { - "${RecipientTable.TABLE_NAME}.$column = ${RecipientTable.NotificationSetting.ALWAYS_NOTIFY.id}" - } - } - /** * Returns whether or not there are chats in a chat folder */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UnreadReminderJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/UnreadReminderJob.kt index b407bfd0b0..6a1b7c68d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UnreadReminderJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/UnreadReminderJob.kt @@ -98,9 +98,8 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime } val hasNewMessages = SignalDatabase.messages.hasUnreadMessagesSince(threadId, lastReminderTime) - val hasNewCalls = SignalDatabase.calls.hasUnreadCallsSince(threadId, lastReminderTime) - if (!hasNewMessages && !hasNewCalls) { - Log.i(TAG, "No new unread messages or calls for thread $threadId since last reminder. Skipping.") + if (!hasNewMessages) { + Log.i(TAG, "No new unread messages for thread $threadId since last reminder. Skipping.") return Result.success() } @@ -109,8 +108,6 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime // Get the unread counts/authors val (messages, unreadAuthorIds) = getUnreadForReminder(ReminderType.MESSAGES, isEligible = true) stopwatch.split("fetch-messages") - val (calls, callsAuthorIds) = getUnreadForReminder(ReminderType.CALLS, isEligible = recipient.callNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY) - stopwatch.split("fetch-calls") val (mentions, mentionsAuthorIds) = getUnreadForReminder(ReminderType.MENTIONS, isEligible = !hideAuthors && recipient.isPushV2Group && recipient.mentionSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY) stopwatch.split("fetch-mentions") val (replies, repliesAuthorIds) = getUnreadForReminder(ReminderType.REPLIES, isEligible = !hideAuthors && recipient.isPushV2Group && recipient.replyNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY) @@ -119,10 +116,9 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime val summary = buildSummary( context = context, messages = messages, - calls = calls, mentions = mentions, replies = replies, - messageAndCallAuthors = (unreadAuthorIds + callsAuthorIds).distinct().map { Recipient.resolved(it).getShortDisplayName(context) }, + messageAuthors = unreadAuthorIds.map { Recipient.resolved(it).getShortDisplayName(context) }, mentionAuthors = mentionsAuthorIds.map { Recipient.resolved(it).getShortDisplayName(context) }, replyAuthors = repliesAuthorIds.map { Recipient.resolved(it).getShortDisplayName(context) }, hideAuthors = hideAuthors @@ -169,48 +165,31 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime internal fun buildSummary( context: Context, messages: Int = 0, - calls: Int = 0, mentions: Int = 0, replies: Int = 0, - messageAndCallAuthors: List = emptyList(), + messageAuthors: List = emptyList(), mentionAuthors: List = emptyList(), replyAuthors: List = emptyList(), hideAuthors: Boolean = false ): String { val showUnread = messages > 0 - val showCalls = calls > 0 - val showAuthors = (showUnread || showCalls) && !hideAuthors + val showAuthors = showUnread && !hideAuthors val showMentions = mentions > 0 && !hideAuthors val showReplies = replies > 0 && !hideAuthors val messagesString = if (showUnread) context.resources.getQuantityString(R.plurals.UnreadReminderJob__messages, messages, messages) else "" - val callsString = if (showCalls) context.resources.getQuantityString(R.plurals.UnreadReminderJob__calls, calls, calls) else "" - val authorsString = if (showAuthors) buildAuthorSummary(context, ReminderType.MESSAGES, messages + calls, messageAndCallAuthors) else "" + val authorsString = if (showAuthors) buildAuthorSummary(context, ReminderType.MESSAGES, messages, messageAuthors) else "" val mentionsString = if (showMentions) buildAuthorSummary(context, ReminderType.MENTIONS, mentions, mentionAuthors) else "" val repliesString = if (showReplies) buildAuthorSummary(context, ReminderType.REPLIES, replies, replyAuthors) else "" - return if (showUnread && showCalls && showMentions && showReplies) { - context.getString(R.string.UnreadReminderJob__calls_and_unread_full_summary, callsString, messagesString, mentionsString, repliesString) - } else if (showUnread && showCalls && showMentions) { - context.getString(R.string.UnreadReminderJob__calls_and_unread_summary, callsString, messagesString, mentionsString) - } else if (showUnread && showCalls && showReplies) { - context.getString(R.string.UnreadReminderJob__calls_and_unread_summary, callsString, messagesString, repliesString) - } else if (showUnread && showCalls && hideAuthors) { - context.getString(R.string.UnreadReminderJob__calls_and_unread, callsString, messagesString) - } else if (showUnread && showCalls) { - context.getString(R.string.UnreadReminderJob__calls_and_unread_author, callsString, messagesString, authorsString) - } else if (showUnread && showMentions && showReplies) { + return if (showUnread && showMentions && showReplies) { context.getString(R.string.UnreadReminderJob__unread_both_summary, messagesString, mentionsString, repliesString) } else if (showUnread && showMentions) { context.getString(R.string.UnreadReminderJob__unread_one_summary, messagesString, mentionsString) } else if (showUnread && showReplies) { context.getString(R.string.UnreadReminderJob__unread_one_summary, messagesString, repliesString) - } else if (hideAuthors && showCalls) { - context.getString(R.string.UnreadReminderJob__calls_or_unread, callsString) } else if (hideAuthors && showUnread) { context.getString(R.string.UnreadReminderJob__calls_or_unread, messagesString) - } else if (showCalls) { - context.getString(R.string.UnreadReminderJob__calls_or_unread_author, callsString, authorsString) } else if (showUnread) { context.getString(R.string.UnreadReminderJob__calls_or_unread_author, messagesString, authorsString) } else { @@ -225,8 +204,7 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime @VisibleForTesting internal fun buildAuthorSummary(context: Context, reminderType: ReminderType, count: Int, authors: List): String { val (oneRes, twoRes, manyRes) = when (reminderType) { - ReminderType.MESSAGES, - ReminderType.CALLS -> Triple(R.string.UnreadReminderJob__authors_one, R.string.UnreadReminderJob__authors_two, R.string.UnreadReminderJob__authors_many) + ReminderType.MESSAGES -> Triple(R.string.UnreadReminderJob__authors_one, R.string.UnreadReminderJob__authors_two, R.string.UnreadReminderJob__authors_many) ReminderType.MENTIONS -> Triple(R.string.UnreadReminderJob__mentions_one, R.plurals.UnreadReminderJob__mentions_two, R.plurals.UnreadReminderJob__mentions_many) ReminderType.REPLIES -> Triple(R.string.UnreadReminderJob__replies_one, R.plurals.UnreadReminderJob__replies_two, R.plurals.UnreadReminderJob__replies_many) } @@ -250,8 +228,6 @@ class UnreadReminderJob(private val threadId: Long, private val lastReminderTime private fun getUnreadForReminder(reminderType: ReminderType, isEligible: Boolean): Pair> { return if (!isEligible) { 0 to emptyList() - } else if (reminderType == ReminderType.CALLS) { - SignalDatabase.calls.getUnreadCallsForReminderNotification(threadId) } else { SignalDatabase.messages.getUnreadContentForReminderNotification(threadId, reminderType) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/UnreadReminderManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/UnreadReminderManager.kt index 4e82ecdd7e..f5a00253c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/UnreadReminderManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/UnreadReminderManager.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import androidx.annotation.WorkerThread import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.components.settings.app.notifications.ReminderType import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.UnreadReminderJob @@ -35,24 +34,17 @@ class UnreadReminderManager( @WorkerThread override fun getNextClosestEvent(): Event? { - val messageThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.MESSAGES, reminderThreshold) - val callThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.CALLS, reminderThreshold) - + val messageThreadIds = SignalDatabase.threads.getMutedThreadIds(reminderThreshold) val (messageThreadId, messageTimestamp) = SignalDatabase.messages.getOldestUnreadMessage(messageThreadIds) - val (callThreadId, callTimestamp) = SignalDatabase.calls.getOldestUnreadCall(callThreadIds) - return if (messageThreadId == -1L && callThreadId == -1L) { + return if (messageThreadId == -1L) { Log.i(TAG, "No existing unread message or calls from a qualifying thread.") cancelAlarm(application, UnreadReminderAlarm::class.java) null - } else if (messageTimestamp < callTimestamp) { + } else { val delay = (messageTimestamp + reminderThreshold - System.currentTimeMillis()).coerceAtLeast(0) Log.i(TAG, "The next unread reminder needs to fire in $delay ms for a message in thread $messageThreadId.") Event(delay, messageThreadId) - } else { - val delay = (callTimestamp + reminderThreshold - System.currentTimeMillis()).coerceAtLeast(0) - Log.i(TAG, "The next unread reminder needs to fire in $delay ms for a call in thread $callThreadId.") - Event(delay, callThreadId) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 07fbab50e3..d3121aaeea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9959,27 +9959,14 @@ Don\'t share - - You have %1$s and %2$s including %3$s and %4$s. - - You have %1$s and %2$s including %3$s. - - You have %1$s and %2$s from %3$s. You have %1$s, including %2$s. You have %1$s, including %2$s and %3$s. You have %1$s from %2$s. - + You have %1$s. - - You have %1$s and %2$s. - - - %1$d missed call - %1$d missed calls - %1$d unread message diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/CallTableTest_unreadForReminder.kt b/app/src/test/java/org/thoughtcrime/securesms/database/CallTableTest_unreadForReminder.kt deleted file mode 100644 index 1dd6aec4ef..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/database/CallTableTest_unreadForReminder.kt +++ /dev/null @@ -1,115 +0,0 @@ -package org.thoughtcrime.securesms.database - -import android.app.Application -import assertk.assertThat -import assertk.assertions.isEqualTo -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.testutil.RecipientTestRule - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, application = Application::class) -class CallTableTest_unreadForReminder { - - @get:Rule - val recipients = RecipientTestRule() - - private val calls: CallTable - get() = SignalDatabase.calls - - private var nextCallId = 1L - - @Test - fun `counts unread missed calls from a single caller`() { - val caller = recipients.createRecipient("Alice Android") - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(caller)) - - insertMissedCall(caller, time = 1000) - insertMissedCall(caller, time = 1001) - - val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1001) - - assertThat(count).isEqualTo(2) - assertThat(authors).isEqualTo(listOf(caller)) - } - - @Test - fun `a missed-notification-profile call also counts as missed`() { - val caller = recipients.createRecipient("Bob Caller") - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(caller)) - - insertCall(caller, time = 1000, event = CallTable.Event.MISSED_NOTIFICATION_PROFILE) - - val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1001) - - assertThat(count).isEqualTo(1) - assertThat(authors).isEqualTo(listOf(caller)) - } - - @Test - fun `non-missed call events are excluded`() { - val caller = recipients.createRecipient("Carol Answered") - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(caller)) - - insertCall(caller, time = 1000, event = CallTable.Event.ACCEPTED) - insertMissedCall(caller, time = 1001) - - val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1002) - - assertThat(count).isEqualTo(1) - assertThat(authors).isEqualTo(listOf(caller)) - } - - @Test - fun `already-read missed calls are excluded`() { - val caller = recipients.createRecipient("Dave Rung") - val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(caller)) - - insertMissedCall(caller, time = 1000) - calls.markAllCallEventsRead(timestamp = 1000) - insertMissedCall(caller, time = 1001) - - val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1002) - - assertThat(count).isEqualTo(1) - assertThat(authors).isEqualTo(listOf(caller)) - } - - @Test - fun `only the requested threads are counted`() { - val included = recipients.createRecipient("Erin Included") - val excluded = recipients.createRecipient("Frank Excluded") - val includedThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(included)) - SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(excluded)) - - insertMissedCall(included, time = 1000) - insertMissedCall(excluded, time = 1000) - - val (count, authors) = calls.getUnreadCallsForReminderNotification(includedThreadId, 1001) - - assertThat(count).isEqualTo(1) - assertThat(authors).isEqualTo(listOf(included)) - } - - private fun insertMissedCall(caller: RecipientId, time: Long): Long { - return insertCall(caller, time, CallTable.Event.MISSED) - } - - private fun insertCall(caller: RecipientId, time: Long, event: CallTable.Event): Long { - val callId = nextCallId++ - calls.insertOneToOneCall( - callId = callId, - timestamp = time, - peer = caller, - type = CallTable.Type.AUDIO_CALL, - direction = CallTable.Direction.INCOMING, - event = event - ) - return callId - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_mutedThreadIds.kt b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_mutedThreadIds.kt index 725e9211c1..c283c7bcfb 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_mutedThreadIds.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/ThreadTableTest_mutedThreadIds.kt @@ -12,7 +12,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -import org.thoughtcrime.securesms.components.settings.app.notifications.ReminderType import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.testutil.RecipientTestRule @@ -36,15 +35,14 @@ class ThreadTableTest_mutedThreadIds { SignalDatabase.recipients.setMuted(contactId, Long.MAX_VALUE) } - private fun globalDefaults(unreadReminder: Boolean = true, calls: Boolean = false, mentions: Boolean = true, replies: Boolean = true) { + private fun globalDefaults(unreadReminder: Boolean = true, mentions: Boolean = true, replies: Boolean = true) { every { recipients.signalStore.settings.unreadReminderEnabled } returns unreadReminder - every { recipients.signalStore.settings.allowCallsWhileMuted } returns calls every { recipients.signalStore.settings.allowMentionsWhileMuted } returns mentions every { recipients.signalStore.settings.allowRepliesWhileMuted } returns replies } - private fun isMutedFor(reminderType: ReminderType, threshold: Long = 0): Boolean { - return threadId in SignalDatabase.threads.getMutedThreadIds(reminderType, threshold) + private fun isMutedFor(threshold: Long = 0): Boolean { + return threadId in SignalDatabase.threads.getMutedThreadIds(threshold) } @Test @@ -52,14 +50,14 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT) SignalDatabase.threads.incrementUnread(threadId, 1, 1) - assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue() + assertThat(isMutedFor()).isTrue() } @Test fun `allow-by-default plus an explicit opt-out excludes the thread`() { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.DO_NOT_NOTIFY) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() + assertThat(isMutedFor()).isFalse() } @Test @@ -67,21 +65,21 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.threads.incrementUnread(threadId, 1, 1) - assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue() + assertThat(isMutedFor()).isTrue() } @Test fun `always-allow-required plus system-default recipient setting excludes the thread`() { globalDefaults(unreadReminder = false) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() + assertThat(isMutedFor()).isFalse() } @Test fun `always-allow-required plus an explicit opt-out excludes the thread`() { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.DO_NOT_NOTIFY) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() + assertThat(isMutedFor()).isFalse() } @Test @@ -89,7 +87,7 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = false) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.threads.incrementUnread(threadId, 1, 1) - assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue() + assertThat(isMutedFor()).isTrue() } @Test @@ -97,7 +95,7 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.recipients.setMuted(contactId, 0L) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() + assertThat(isMutedFor()).isFalse() } @Test @@ -105,7 +103,7 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.threads.setUnreadReminderTime(threadId, System.currentTimeMillis()) - assertThat(isMutedFor(ReminderType.MESSAGES, 3.days.inWholeMilliseconds)).isFalse() + assertThat(isMutedFor(3.days.inWholeMilliseconds)).isFalse() } @Test @@ -114,14 +112,14 @@ class ThreadTableTest_mutedThreadIds { SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.threads.setUnreadReminderTime(threadId, System.currentTimeMillis() - 4.days.inWholeMilliseconds) SignalDatabase.threads.incrementUnread(threadId, 1, 1) - assertThat(isMutedFor(ReminderType.MESSAGES, 3.days.inWholeMilliseconds)).isTrue() + assertThat(isMutedFor(3.days.inWholeMilliseconds)).isTrue() } @Test fun `a thread that does not have any unread is not included`() { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() + assertThat(isMutedFor()).isFalse() } @Test @@ -129,30 +127,6 @@ class ThreadTableTest_mutedThreadIds { globalDefaults(unreadReminder = true) SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) SignalDatabase.threads.setArchived(setOf(threadId), true) - assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse() - } - - @Test - fun `calls, allow-by-default false, require an explicit always-allow for calls specifically`() { - globalDefaults(unreadReminder = true, calls = false) - SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) - - SignalDatabase.recipients.setCallNotificationSetting(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT) - assertThat(isMutedFor(ReminderType.CALLS)).isFalse() - - SignalDatabase.recipients.setCallNotificationSetting(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) - assertThat(isMutedFor(ReminderType.CALLS)).isTrue() - } - - @Test - fun `calls, allow-by-default true, an explicit opt-out for calls specifically still excludes it`() { - globalDefaults(unreadReminder = true, calls = true) - SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY) - - SignalDatabase.recipients.setCallNotificationSetting(contactId, RecipientTable.NotificationSetting.DO_NOT_NOTIFY) - assertThat(isMutedFor(ReminderType.CALLS)).isFalse() - - SignalDatabase.recipients.setCallNotificationSetting(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT) - assertThat(isMutedFor(ReminderType.CALLS)).isTrue() + assertThat(isMutedFor()).isFalse() } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobs/UnreadReminderJobTest.kt b/app/src/test/java/org/thoughtcrime/securesms/jobs/UnreadReminderJobTest.kt index fab5cdcbe7..c0459b8f29 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/jobs/UnreadReminderJobTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/jobs/UnreadReminderJobTest.kt @@ -40,12 +40,6 @@ class UnreadReminderJobTest { assertEquals("Alice and others", result) } - @Test - fun `calls, two authors, shares the same wording as messages`() { - val result = UnreadReminderJob.create().buildAuthorSummary(context, ReminderType.CALLS, 5, listOf("Alice", "Bob")) - assertEquals("Alice and Bob", result) - } - @Test fun `mentions, one author, omits the count`() { val result = UnreadReminderJob.create().buildAuthorSummary(context, ReminderType.MENTIONS, 1, listOf("Dave")) @@ -90,140 +84,48 @@ class UnreadReminderJobTest { @Test fun `messages only, singular message and singular author`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 1, messageAndCallAuthors = listOf("Alice")) + val result = UnreadReminderJob.create().buildSummary(context, messages = 1, messageAuthors = listOf("Alice")) assertEquals("You have 1 unread message from Alice.", result) } @Test fun `messages only, plural messages and two authors`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 5, messageAndCallAuthors = listOf("Alice", "Bob")) + val result = UnreadReminderJob.create().buildSummary(context, messages = 5, messageAuthors = listOf("Alice", "Bob")) assertEquals("You have 5 unread messages from Alice and Bob.", result) } @Test - fun `calls only, singular call and singular author`() { - val result = UnreadReminderJob.create().buildSummary(context, calls = 1, messageAndCallAuthors = listOf("Bob")) - assertEquals("You have 1 missed call from Bob.", result) - } - - @Test - fun `calls only, plural calls and three or more authors`() { - val result = UnreadReminderJob.create().buildSummary(context, calls = 4, messageAndCallAuthors = listOf("Carol", "Dave", "Erin")) - assertEquals("You have 4 missed calls from Carol and others.", result) - } - - @Test - fun `messages and calls, no mentions or replies, combines counts and authors`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 2, calls = 1, messageAndCallAuthors = listOf("Alice", "Bob")) - assertEquals("You have 1 missed call and 2 unread messages from Alice and Bob.", result) - } - - @Test - fun `messages, calls, and mentions uses the calls-and-unread summary with mentions`() { + fun `messages and mentions, combines counts and authors`() { val result = UnreadReminderJob.create().buildSummary( context, messages = 1, - calls = 1, mentions = 1, - messageAndCallAuthors = listOf("Alice"), + messageAuthors = listOf("Alice"), mentionAuthors = listOf("Dave") ) - assertEquals("You have 1 missed call and 1 unread message including a mention of you by Dave.", result) + assertEquals("You have 1 unread message, including a mention of you by Dave.", result) } @Test - fun `messages, calls, and replies uses the calls-and-unread summary with replies`() { + fun `messages and replies, combines counts and authors`() { val result = UnreadReminderJob.create().buildSummary( context, messages = 1, - calls = 1, replies = 1, - messageAndCallAuthors = listOf("Alice"), + messageAuthors = listOf("Alice"), replyAuthors = listOf("Grace") ) - assertEquals("You have 1 missed call and 1 unread message including a reply from Grace.", result) - } - - @Test - fun `messages, calls, mentions, and replies all true uses the full summary`() { - val result = UnreadReminderJob.create().buildSummary( - context, - messages = 1, - calls = 1, - mentions = 1, - replies = 1, - messageAndCallAuthors = listOf("Alice"), - mentionAuthors = listOf("Dave"), - replyAuthors = listOf("Grace") - ) - assertEquals("You have 1 missed call and 1 unread message including a mention of you by Dave and a reply from Grace.", result) - } - - @Test - fun `messages, calls, mentions, and replies all true, plural messages and calls`() { - val result = UnreadReminderJob.create().buildSummary( - context, - messages = 3, - calls = 2, - mentions = 1, - replies = 1, - messageAndCallAuthors = listOf("Alice", "Bob"), - mentionAuthors = listOf("Dave"), - replyAuthors = listOf("Grace") - ) - assertEquals("You have 2 missed calls and 3 unread messages including a mention of you by Dave and a reply from Grace.", result) - } - - @Test - fun `messages, calls, mentions, and replies all true, two mention and reply authors`() { - val result = UnreadReminderJob.create().buildSummary( - context, - messages = 1, - calls = 1, - mentions = 2, - replies = 2, - messageAndCallAuthors = listOf("Alice"), - mentionAuthors = listOf("Dave", "Erin"), - replyAuthors = listOf("Grace", "Heidi") - ) - assertEquals("You have 1 missed call and 1 unread message including 2 mentions of you by Dave and Erin and 2 replies from Grace and Heidi.", result) - } - - @Test - fun `messages, calls, mentions, and replies all true, three or more mention and reply authors`() { - val result = UnreadReminderJob.create().buildSummary( - context, - messages = 1, - calls = 1, - mentions = 5, - replies = 4, - messageAndCallAuthors = listOf("Alice"), - mentionAuthors = listOf("Dave", "Erin", "Frank"), - replyAuthors = listOf("Grace", "Heidi", "Ivan") - ) - assertEquals("You have 1 missed call and 1 unread message including 5 mentions of you by Dave and others and 4 replies from Grace and others.", result) - } - - @Test - fun `messages and mentions, no calls`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 2, mentions = 2, messageAndCallAuthors = listOf("Alice"), mentionAuthors = listOf("Frank", "Erin")) - assertEquals("You have 2 unread messages, including 2 mentions of you by Frank and Erin.", result) - } - - @Test - fun `messages and replies, no calls`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 1, replies = 3, messageAndCallAuthors = listOf("Alice"), replyAuthors = listOf("Grace")) assertEquals("You have 1 unread message, including a reply from Grace.", result) } @Test - fun `messages, mentions, and replies all true, no calls - both are shown`() { + fun `messages, mentions, and replies all true uses the full summary`() { val result = UnreadReminderJob.create().buildSummary( context, messages = 1, mentions = 1, replies = 1, - messageAndCallAuthors = listOf("Alice"), + messageAuthors = listOf("Alice"), mentionAuthors = listOf("Dave"), replyAuthors = listOf("Grace") ) @@ -231,61 +133,65 @@ class UnreadReminderJobTest { } @Test - fun `calls and mentions, no unread messages - falls back to calls-or-unread-author with calls`() { - val result = UnreadReminderJob.create().buildSummary(context, calls = 1, mentions = 1, messageAndCallAuthors = listOf("Bob"), mentionAuthors = listOf("Dave")) - assertEquals("You have 1 missed call from Bob.", result) + fun `messages, mentions, and replies all true, plural messages`() { + val result = UnreadReminderJob.create().buildSummary( + context, + messages = 3, + mentions = 1, + replies = 1, + messageAuthors = listOf("Alice", "Bob"), + mentionAuthors = listOf("Dave"), + replyAuthors = listOf("Grace") + ) + assertEquals("You have 3 unread messages, including a mention of you by Dave and a reply from Grace.", result) + } + + @Test + fun `messages, mentions, and replies all true, two mention and reply authors`() { + val result = UnreadReminderJob.create().buildSummary( + context, + messages = 1, + mentions = 2, + replies = 2, + messageAuthors = listOf("Alice"), + mentionAuthors = listOf("Dave", "Erin"), + replyAuthors = listOf("Grace", "Heidi") + ) + assertEquals("You have 1 unread message, including 2 mentions of you by Dave and Erin and 2 replies from Grace and Heidi.", result) + } + + @Test + fun `messages, mentions, and replies all true, three or more mention and reply authors`() { + val result = UnreadReminderJob.create().buildSummary( + context, + messages = 1, + mentions = 5, + replies = 4, + messageAuthors = listOf("Alice"), + mentionAuthors = listOf("Dave", "Erin", "Frank"), + replyAuthors = listOf("Grace", "Heidi", "Ivan") + ) + assertEquals("You have 1 unread message, including 5 mentions of you by Dave and others and 4 replies from Grace and others.", result) } @Test fun `hideAuthors, messages only, omits author names`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 1, messageAndCallAuthors = listOf("Alice"), hideAuthors = true) + val result = UnreadReminderJob.create().buildSummary(context, messages = 1, messageAuthors = listOf("Alice"), hideAuthors = true) assertEquals("You have 1 unread message.", result) } @Test - fun `hideAuthors, calls only, omits author names`() { - val result = UnreadReminderJob.create().buildSummary(context, calls = 1, messageAndCallAuthors = listOf("Bob"), hideAuthors = true) - assertEquals("You have 1 missed call.", result) - } - - @Test - fun `hideAuthors, messages and calls, uses calls-and-unread without authors`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 2, calls = 1, messageAndCallAuthors = listOf("Alice", "Bob"), hideAuthors = true) - assertEquals("You have 1 missed call and 2 unread messages.", result) - } - - @Test - fun `hideAuthors, messages, calls, and mentions, mentions are dropped entirely`() { - val result = UnreadReminderJob.create().buildSummary( - context, - messages = 1, - calls = 1, - mentions = 1, - messageAndCallAuthors = listOf("Alice"), - mentionAuthors = listOf("Dave"), - hideAuthors = true - ) - assertEquals("You have 1 missed call and 1 unread message.", result) - } - - @Test - fun `hideAuthors, messages and mentions, no calls, mentions are dropped entirely`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 2, mentions = 2, messageAndCallAuthors = listOf("Alice"), mentionAuthors = listOf("Frank", "Erin"), hideAuthors = true) + fun `hideAuthors, messages and mentions, mentions are dropped entirely`() { + val result = UnreadReminderJob.create().buildSummary(context, messages = 2, mentions = 2, messageAuthors = listOf("Alice"), mentionAuthors = listOf("Frank", "Erin"), hideAuthors = true) assertEquals("You have 2 unread messages.", result) } @Test - fun `hideAuthors, messages and replies, no calls, replies are dropped entirely`() { - val result = UnreadReminderJob.create().buildSummary(context, messages = 1, replies = 3, messageAndCallAuthors = listOf("Alice"), replyAuthors = listOf("Grace"), hideAuthors = true) + fun `hideAuthors, messages and replies, replies are dropped entirely`() { + val result = UnreadReminderJob.create().buildSummary(context, messages = 1, replies = 3, messageAuthors = listOf("Alice"), replyAuthors = listOf("Grace"), hideAuthors = true) assertEquals("You have 1 unread message.", result) } - @Test - fun `hideAuthors, calls and mentions, no unread messages, mentions are dropped entirely`() { - val result = UnreadReminderJob.create().buildSummary(context, calls = 1, mentions = 1, messageAndCallAuthors = listOf("Bob"), mentionAuthors = listOf("Dave"), hideAuthors = true) - assertEquals("You have 1 missed call.", result) - } - @Test fun `hideAuthors, nothing unread produces an empty summary`() { val result = UnreadReminderJob.create().buildSummary(context, hideAuthors = true)