mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 08:45:50 +01:00
Update unread reminder system.
This commit is contained in:
@@ -257,6 +257,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
|
||||
.addPostRender(RestoreOptimizedMediaJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> AppDependencies.getPinnedMessageManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> AppDependencies.getUnreadReminderManager().scheduleIfNecessary())
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
+4
-2
@@ -42,6 +42,7 @@ 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
|
||||
@@ -454,9 +455,10 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Run unread reminder job"),
|
||||
summary = DSLSettingsText.from("Generates an unread reminder notification based on unreads and muted preferences."),
|
||||
summary = DSLSettingsText.from("Generates an unread reminder notification based on unreads and muted preferences. Skips the three day cooldown."),
|
||||
onClick = {
|
||||
UnreadReminderJob.enqueue()
|
||||
val threadIds = (SignalDatabase.threads.getMutedThreadIds(ReminderType.MESSAGES, 0) + SignalDatabase.threads.getMutedThreadIds(ReminderType.CALLS, 0)).distinct()
|
||||
threadIds.forEach { threadId -> UnreadReminderJob.enqueue(threadId, 0) }
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ 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,17 +195,18 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of unread calls and up to three distinct callers from eligible [threadIds].
|
||||
* Returns the number of unread calls and up to three distinct callers from [threadId].
|
||||
*/
|
||||
fun getUnreadCallsForReminderNotification(threadIds: List<Long>): Pair<Int, List<RecipientId>> {
|
||||
fun getUnreadCallsForReminderNotification(threadId: Long, now: Long = System.currentTimeMillis()): Pair<Int, List<RecipientId>> {
|
||||
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} IN (${threadIds.joinToString(",")}) AND
|
||||
${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)})
|
||||
($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")
|
||||
@@ -214,6 +216,52 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||
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<Long>): Pair<Long, Long> {
|
||||
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)
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo
|
||||
import org.thoughtcrime.securesms.service.UnreadReminderManager
|
||||
import org.thoughtcrime.securesms.sms.GroupV2UpdateMessageUtil
|
||||
import org.thoughtcrime.securesms.stories.Stories.isFeatureEnabled
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
@@ -5400,10 +5401,10 @@ 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 eligible [threadIds].
|
||||
* 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(threadIds: List<Long>, type: ReminderType): Pair<Int, List<RecipientId>> {
|
||||
fun getUnreadContentForReminderNotification(threadId: Long, type: ReminderType, now: Long = System.currentTimeMillis()): Pair<Int, List<RecipientId>> {
|
||||
val categoryClause = when (type) {
|
||||
ReminderType.MENTIONS -> "AND $MENTIONS_SELF = 1"
|
||||
ReminderType.REPLIES -> "AND $QUOTE_AUTHOR = ${Recipient.self().id.serialize()}"
|
||||
@@ -5415,13 +5416,14 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_UNREAD_COUNT")
|
||||
.where(
|
||||
"""
|
||||
$THREAD_ID IN (${threadIds.joinToString(",")}) AND
|
||||
$READ = 0 AND
|
||||
$THREAD_ID = $threadId AND
|
||||
$STORY_TYPE = 0 AND
|
||||
$PARENT_STORY_ID <= 0 AND
|
||||
$ORIGINAL_MESSAGE_ID IS NULL AND
|
||||
$SCHEDULED_DATE = -1 AND
|
||||
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE}
|
||||
$ORIGINAL_MESSAGE_ID IS NULL AND
|
||||
$READ = 0 AND
|
||||
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE} AND
|
||||
$DATE_RECEIVED > ${now - UnreadReminderManager.MAX_UNREAD_MESSAGE_AGE.inWholeMilliseconds}
|
||||
$categoryClause
|
||||
"""
|
||||
)
|
||||
@@ -5432,6 +5434,60 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return authorIds.size to authorIds.distinct().take(3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if [threadId] has an unread message that arrived after [since].
|
||||
*/
|
||||
fun hasUnreadMessagesSince(threadId: Long, since: Long): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where(
|
||||
"""
|
||||
$THREAD_ID = $threadId AND
|
||||
$STORY_TYPE = 0 AND
|
||||
$PARENT_STORY_ID <= 0 AND
|
||||
$SCHEDULED_DATE = -1 AND
|
||||
$ORIGINAL_MESSAGE_ID IS NULL AND
|
||||
$READ = 0 AND
|
||||
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE} AND
|
||||
$DATE_RECEIVED > $since
|
||||
"""
|
||||
)
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thread id and timestamp of the oldest unread message across [threadIds].
|
||||
* If there is none, it returns -1 for thread id and Long.MAX_VALUE for timestamp.
|
||||
*/
|
||||
fun getOldestUnreadMessage(threadIds: List<Long>): Pair<Long, Long> {
|
||||
if (threadIds.isEmpty()) {
|
||||
return Pair(-1, Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
val query = SqlUtil.buildFastCollectionQuery(THREAD_ID, threadIds)
|
||||
|
||||
return readableDatabase
|
||||
.select(THREAD_ID, DATE_RECEIVED)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_UNREAD_COUNT")
|
||||
.where(
|
||||
"""
|
||||
${query.where} AND
|
||||
$STORY_TYPE = 0 AND
|
||||
$PARENT_STORY_ID <= 0 AND
|
||||
$SCHEDULED_DATE = -1 AND
|
||||
$ORIGINAL_MESSAGE_ID IS NULL AND
|
||||
$READ = 0 AND
|
||||
($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) != ${MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE} AND
|
||||
$DATE_RECEIVED > ${System.currentTimeMillis() - UnreadReminderManager.MAX_UNREAD_MESSAGE_AGE.inWholeMilliseconds}
|
||||
""",
|
||||
query.whereArgs
|
||||
)
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.readToSingleObject { cursor -> cursor.requireLong(THREAD_ID) to cursor.requireLong(DATE_RECEIVED) } ?: Pair(-1, Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
fun messageExists(messageRecord: MessageRecord): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
|
||||
@@ -127,6 +127,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
const val PINNED_ORDER = "pinned_order"
|
||||
const val UNREAD_SELF_MENTION_COUNT = "unread_self_mention_count"
|
||||
const val ACTIVE = "active"
|
||||
const val LAST_UNREAD_REMINDER = "last_unread_reminder"
|
||||
|
||||
const val MAX_CACHE_SIZE = 1000
|
||||
|
||||
@@ -158,7 +159,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
$UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0,
|
||||
$ACTIVE INTEGER DEFAULT 0,
|
||||
$SNIPPET_MESSAGE_EXTRAS BLOB DEFAULT NULL,
|
||||
$SNIPPET_MESSAGE_ID INTEGER DEFAULT 0
|
||||
$SNIPPET_MESSAGE_ID INTEGER DEFAULT 0,
|
||||
$LAST_UNREAD_REMINDER INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -681,25 +683,48 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
* 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.
|
||||
*/
|
||||
fun getMutedThreadIds(reminderType: ReminderType): List<Long> {
|
||||
val isGV2Clause = "${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} = ${RecipientTable.RecipientType.GV2.id}"
|
||||
fun getMutedThreadIds(reminderType: ReminderType, reminderThreshold: Long, now: Long = System.currentTimeMillis()): List<Long> {
|
||||
val unreadReminderClause = getNotificationClause(RecipientTable.UNREAD_REMINDER, SignalStore.settings.unreadReminderEnabled)
|
||||
|
||||
val reminderClause = when (reminderType) {
|
||||
ReminderType.MESSAGES -> unreadReminderClause
|
||||
ReminderType.CALLS -> "$unreadReminderClause AND (${getNotificationClause(RecipientTable.CALL_NOTIFICATION_SETTING, SignalStore.settings.allowCallsWhileMuted)})"
|
||||
ReminderType.MENTIONS -> "$unreadReminderClause AND $isGV2Clause AND (${getNotificationClause(RecipientTable.MENTION_SETTING, SignalStore.settings.allowMentionsWhileMuted)})"
|
||||
ReminderType.REPLIES -> "$unreadReminderClause AND $isGV2Clause AND (${getNotificationClause(RecipientTable.REPLY_NOTIFICATION_SETTING, SignalStore.settings.allowRepliesWhileMuted)})"
|
||||
ReminderType.MESSAGES -> "AND $unreadReminderClause AND $UNREAD_COUNT > 0"
|
||||
ReminderType.CALLS -> "AND $unreadReminderClause AND (${getNotificationClause(RecipientTable.CALL_NOTIFICATION_SETTING, SignalStore.settings.allowCallsWhileMuted)})"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
.select("$TABLE_NAME.$ID")
|
||||
.from("$TABLE_NAME INNER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}")
|
||||
.where("$ARCHIVED = 0 AND ${RecipientTable.MUTE_UNTIL} >= ${System.currentTimeMillis()} AND $reminderClause")
|
||||
.where(
|
||||
"""
|
||||
$ACTIVE = 1 AND
|
||||
$ARCHIVED = 0 AND
|
||||
$LAST_UNREAD_REMINDER < ${now - reminderThreshold} AND
|
||||
${RecipientTable.MUTE_UNTIL} >= $now
|
||||
$reminderClause
|
||||
""".trimIndent()
|
||||
)
|
||||
.run()
|
||||
.readToList { it.requireLong(ID) }
|
||||
}
|
||||
|
||||
fun getUnreadReminderTime(threadId: Long): Long {
|
||||
return readableDatabase
|
||||
.select(LAST_UNREAD_REMINDER)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", threadId)
|
||||
.run()
|
||||
.readToSingleLong(0)
|
||||
}
|
||||
|
||||
fun setUnreadReminderTime(threadId: Long, timestamp: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(LAST_UNREAD_REMINDER to timestamp)
|
||||
.where("$ID = ?", threadId)
|
||||
.run()
|
||||
}
|
||||
|
||||
private fun getNotificationClause(column: String, allowByDefault: Boolean): String {
|
||||
return if (allowByDefault) {
|
||||
"${RecipientTable.TABLE_NAME}.$column != ${RecipientTable.NotificationSetting.DO_NOT_NOTIFY.id}"
|
||||
|
||||
+4
-2
@@ -179,6 +179,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V323_AddStickerPack
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V324_MoveGroupV1StorageIdsToUnknownIds
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V325_AddBlockedAtToRecipientTable
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V326_AddUnreadReminderColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V327_AddLastUnreadReminderColumn
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -365,10 +366,11 @@ object SignalDatabaseMigrations {
|
||||
323 to V323_AddStickerPackStorageSync,
|
||||
324 to V324_MoveGroupV1StorageIdsToUnknownIds,
|
||||
325 to V325_AddBlockedAtToRecipientTable,
|
||||
326 to V326_AddUnreadReminderColumn
|
||||
326 to V326_AddUnreadReminderColumn,
|
||||
327 to V327_AddLastUnreadReminderColumn
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 326
|
||||
const val DATABASE_VERSION = 327
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds a column to track when a thread last had an unread reminder sent.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V327_AddLastUnreadReminderColumn : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE thread ADD COLUMN last_unread_reminder INTEGER DEFAULT 0")
|
||||
db.execSQL("UPDATE thread SET last_unread_reminder = last_seen")
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.service.PendingRetryReceiptManager
|
||||
import org.thoughtcrime.securesms.service.PinnedMessageManager
|
||||
import org.thoughtcrime.securesms.service.ScheduledMessageManager
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager
|
||||
import org.thoughtcrime.securesms.service.UnreadReminderManager
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
|
||||
import org.thoughtcrime.securesms.shakereport.ShakeToReport
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache
|
||||
@@ -265,6 +266,11 @@ object AppDependencies {
|
||||
provider.provideAndroidCallAudioManager()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val unreadReminderManager: UnreadReminderManager by lazy {
|
||||
provider.provideUnreadReminderManager()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val billingApi: BillingApi by lazy {
|
||||
provider.provideBillingApi()
|
||||
@@ -540,6 +546,7 @@ object AppDependencies {
|
||||
fun provideOkHttpClient(): OkHttpClient
|
||||
fun provideScheduledMessageManager(): ScheduledMessageManager
|
||||
fun providePinnedMessageManager(): PinnedMessageManager
|
||||
fun provideUnreadReminderManager(): UnreadReminderManager
|
||||
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
|
||||
fun provideBillingApi(): BillingApi
|
||||
fun provideArchiveApi(pushServiceSocket: PushServiceSocket): ArchiveApi
|
||||
|
||||
+6
@@ -109,6 +109,7 @@ import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.service.PinnedMessageManager;
|
||||
import org.thoughtcrime.securesms.service.ScheduledMessageManager;
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
|
||||
import org.thoughtcrime.securesms.service.UnreadReminderManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
@@ -338,6 +339,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
return new PinnedMessageManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull UnreadReminderManager provideUnreadReminderManager() {
|
||||
return new UnreadReminderManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) {
|
||||
Network network = new Network(BuildConfig.LIBSIGNAL_NET_ENV, StandardUserAgentInterceptor.USER_AGENT, RemoteConfig.getLibsignalConfigs(), Network.BuildVariant.PRODUCTION);
|
||||
|
||||
@@ -5,44 +5,59 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.LocusIdCompat
|
||||
import org.signal.core.util.PendingIntentFlags
|
||||
import org.signal.core.util.ServiceUtil
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.ReminderType
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobs.protos.UnreadReminderJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.notifications.v2.getContactDrawable
|
||||
import org.thoughtcrime.securesms.notifications.v2.makeUniqueToPreventMerging
|
||||
import org.thoughtcrime.securesms.notifications.v2.toLargeBitmap
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
/**
|
||||
* Job that periodically runs and sends an unread reminder for muted chats.
|
||||
* Job that sends an unread reminder notification for a single muted thread.
|
||||
*/
|
||||
class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
class UnreadReminderJob(private val threadId: Long, private val lastReminderTime: Long, parameters: Parameters) : Job(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(UnreadReminderJob::class.java)
|
||||
const val KEY = "UnreadReminderJob"
|
||||
|
||||
@JvmStatic
|
||||
fun enqueue() {
|
||||
fun enqueue(threadId: Long, lastReminderTime: Long) {
|
||||
if (!RemoteConfig.internalUser || !NotificationChannels.getInstance().areNotificationsEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
AppDependencies.jobManager.add(
|
||||
UnreadReminderJob(
|
||||
threadId = threadId,
|
||||
lastReminderTime = lastReminderTime,
|
||||
parameters = Parameters.Builder()
|
||||
.setGlobalPriority(Parameters.PRIORITY_LOWER)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.setQueue("UnreadReminderJob_$threadId")
|
||||
.setMaxInstancesForQueue(1)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
@@ -51,7 +66,9 @@ class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
@VisibleForTesting
|
||||
fun create(): UnreadReminderJob {
|
||||
return UnreadReminderJob(
|
||||
Parameters.Builder()
|
||||
threadId = 1,
|
||||
lastReminderTime = 0,
|
||||
parameters = Parameters.Builder()
|
||||
.setGlobalPriority(Parameters.PRIORITY_LOWER)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.build()
|
||||
@@ -59,7 +76,7 @@ class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
override fun serialize(): ByteArray = UnreadReminderJobData(threadId = threadId, lastReminderTime = lastReminderTime).encode()
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
@@ -70,23 +87,34 @@ class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "Missing recipient for thread $threadId.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (recipient.unreadReminderSetting == RecipientTable.NotificationSetting.DO_NOT_NOTIFY || !recipient.isMuted) {
|
||||
Log.w(TAG, "Recipient ${recipient.id} no longer qualifies for unread reminders")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
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.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val hideAuthors = SignalStore.settings.messageNotificationsPrivacy.isDisplayNothing
|
||||
|
||||
// Get all muted threads that have opted for reminders. If notification privacy is on, ignore mentions and replies.
|
||||
val messageThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.MESSAGES)
|
||||
val callThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.CALLS)
|
||||
val mentionThreadIds = if (hideAuthors) emptyList() else SignalDatabase.threads.getMutedThreadIds(ReminderType.MENTIONS)
|
||||
val replyThreadIds = if (hideAuthors) emptyList() else SignalDatabase.threads.getMutedThreadIds(ReminderType.REPLIES)
|
||||
stopwatch.split("fetch-threads")
|
||||
|
||||
// Get the unread counts/authors
|
||||
val (messages, unreadAuthorIds) = getUnreadForReminder(messageThreadIds, ReminderType.MESSAGES)
|
||||
val (messages, unreadAuthorIds) = getUnreadForReminder(ReminderType.MESSAGES, isEligible = true)
|
||||
stopwatch.split("fetch-messages")
|
||||
val (calls, callsAuthorIds) = getUnreadForReminder(callThreadIds, ReminderType.CALLS)
|
||||
val (calls, callsAuthorIds) = getUnreadForReminder(ReminderType.CALLS, isEligible = recipient.callNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
stopwatch.split("fetch-calls")
|
||||
val (mentions, mentionsAuthorIds) = getUnreadForReminder(mentionThreadIds, ReminderType.MENTIONS)
|
||||
val (mentions, mentionsAuthorIds) = getUnreadForReminder(ReminderType.MENTIONS, isEligible = !hideAuthors && recipient.isPushV2Group && recipient.mentionSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
stopwatch.split("fetch-mentions")
|
||||
val (replies, repliesAuthorIds) = getUnreadForReminder(replyThreadIds, ReminderType.REPLIES)
|
||||
val (replies, repliesAuthorIds) = getUnreadForReminder(ReminderType.REPLIES, isEligible = !hideAuthors && recipient.isPushV2Group && recipient.replyNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
stopwatch.split("fetch-replies")
|
||||
|
||||
val summary = buildSummary(
|
||||
@@ -101,19 +129,36 @@ class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
hideAuthors = hideAuthors
|
||||
)
|
||||
|
||||
val notificationId = NotificationIds.getNotificationIdForUnreadReminder(threadId)
|
||||
|
||||
if (summary.isEmpty()) {
|
||||
ServiceUtil.getNotificationManager(context).cancel(NotificationIds.UNREAD_REMINDER)
|
||||
ServiceUtil.getNotificationManager(context).cancel(notificationId)
|
||||
} else if (NotificationChannels.getInstance().areNotificationsEnabled()) {
|
||||
val builder = NotificationCompat.Builder(context, NotificationChannels.getInstance().ADDITIONAL_MESSAGE_NOTIFICATIONS)
|
||||
val contentIntent = ConversationIntents.createBuilderSync(context, recipient.id, threadId).build().makeUniqueToPreventMerging()
|
||||
val avatar = if (!hideAuthors) recipient.getContactDrawable(context) else FallbackAvatarDrawable(context, FallbackAvatar.forTextOrDefault("Unknown", AvatarColor.UNKNOWN)).circleCrop()
|
||||
|
||||
val person = Person.Builder()
|
||||
.setName(recipient.getDisplayName(context))
|
||||
.setIcon(AvatarUtil.getIconCompat(context, recipient))
|
||||
.build()
|
||||
val messagingStyle: NotificationCompat.MessagingStyle = NotificationCompat.MessagingStyle(Person.Builder().setName(context.getString(R.string.SingleRecipientNotificationBuilder_you)).build())
|
||||
messagingStyle.addMessage(NotificationCompat.MessagingStyle.Message(summary, System.currentTimeMillis(), person))
|
||||
|
||||
val builder = NotificationCompat.Builder(context, NotificationChannels.getInstance().UNREAD_REMINDERS)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(avatar.toLargeBitmap(context))
|
||||
.setContentText(summary)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(summary))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), PendingIntentFlags.mutable()))
|
||||
.setStyle(messagingStyle.takeIf { !hideAuthors })
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setShortcutId(ConversationUtil.getShortcutId(recipient))
|
||||
.setLocusId(LocusIdCompat(ConversationUtil.getShortcutId(recipient)))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntentFlags.updateCurrent()))
|
||||
.setAutoCancel(true)
|
||||
|
||||
ContextCompat.getSystemService(context, NotificationManager::class.java)!!.notify(NotificationIds.UNREAD_REMINDER, builder.build())
|
||||
ContextCompat.getSystemService(context, NotificationManager::class.java)!!.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
SignalDatabase.threads.setUnreadReminderTime(threadId, System.currentTimeMillis())
|
||||
stopwatch.stop(TAG)
|
||||
return Result.success()
|
||||
}
|
||||
@@ -203,23 +248,24 @@ class UnreadReminderJob(parameters: Parameters) : Job(parameters) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUnreadForReminder(threadIds: List<Long>, reminderType: ReminderType): Pair<Int, List<RecipientId>> {
|
||||
return if (threadIds.isEmpty()) {
|
||||
private fun getUnreadForReminder(reminderType: ReminderType, isEligible: Boolean): Pair<Int, List<RecipientId>> {
|
||||
return if (!isEligible) {
|
||||
0 to emptyList()
|
||||
} else if (reminderType == ReminderType.CALLS) {
|
||||
SignalDatabase.calls.getUnreadCallsForReminderNotification(threadIds)
|
||||
SignalDatabase.calls.getUnreadCallsForReminderNotification(threadId)
|
||||
} else {
|
||||
SignalDatabase.messages.getUnreadContentForReminderNotification(threadIds, reminderType)
|
||||
SignalDatabase.messages.getUnreadContentForReminderNotification(threadId, reminderType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
Log.w(TAG, "Failed to create unread reminder notification")
|
||||
Log.w(TAG, "Failed to create unread reminder notification for thread $threadId")
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<UnreadReminderJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): UnreadReminderJob {
|
||||
return UnreadReminderJob(parameters)
|
||||
val data = UnreadReminderJobData.ADAPTER.decode(serializedData!!)
|
||||
return UnreadReminderJob(threadId = data.threadId, lastReminderTime = data.lastReminderTime, parameters = parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -82,6 +82,7 @@ public class NotificationChannels {
|
||||
public final String ADDITIONAL_MESSAGE_NOTIFICATIONS = "additional_message_notifications";
|
||||
public final String NEW_LINKED_DEVICE = "new_linked_device";
|
||||
public final String INTERNAL_ISSUES = "internal_issues";
|
||||
public final String UNREAD_REMINDERS = "unread_reminders";
|
||||
|
||||
private static volatile NotificationChannels instance;
|
||||
|
||||
@@ -638,6 +639,7 @@ public class NotificationChannels {
|
||||
NotificationChannel appAlerts = new NotificationChannel(APP_ALERTS, context.getString(R.string.NotificationChannel_critical_app_alerts), NotificationManager.IMPORTANCE_HIGH);
|
||||
NotificationChannel additionalMessageNotifications = new NotificationChannel(ADDITIONAL_MESSAGE_NOTIFICATIONS, context.getString(R.string.NotificationChannel_additional_message_notifications), NotificationManager.IMPORTANCE_HIGH);
|
||||
NotificationChannel newLinkedDevice = new NotificationChannel(NEW_LINKED_DEVICE, context.getString(R.string.NotificationChannel_new_linked_device), NotificationManager.IMPORTANCE_HIGH);
|
||||
NotificationChannel unreadReminders = new NotificationChannel(UNREAD_REMINDERS, context.getString(R.string.NotificationChannel_unread_reminders), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
messages.setGroup(CATEGORY_MESSAGES);
|
||||
setVibrationEnabled(messages, SignalStore.settings().isMessageVibrateEnabled());
|
||||
@@ -655,7 +657,7 @@ public class NotificationChannels {
|
||||
callStatus.setShowBadge(false);
|
||||
appAlerts.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus, appAlerts, additionalMessageNotifications, newLinkedDevice));
|
||||
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes, joinEvents, background, callStatus, appAlerts, additionalMessageNotifications, newLinkedDevice, unreadReminders));
|
||||
|
||||
if (BuildConfig.MANAGES_APP_UPDATES) {
|
||||
NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
@@ -38,7 +38,8 @@ public final class NotificationIds {
|
||||
public static final int OUT_OF_REMOTE_STORAGE = 1205000;
|
||||
public static final int INITIAL_BACKUP_FAILED = 1205010;
|
||||
public static final int MANUAL_BACKUP_NOT_CREATED = 1205020;
|
||||
public static final int UNREAD_REMINDER = 1205030;
|
||||
public static final int UNREAD_REMINDER = 1206000;
|
||||
public static final int MAX_UNREAD_REMINDER = UNREAD_REMINDER + 100_000;
|
||||
|
||||
private NotificationIds() { }
|
||||
|
||||
@@ -61,4 +62,8 @@ public final class NotificationIds {
|
||||
public static boolean isMessageNotificationId(int id) {
|
||||
return (id >= THREAD && id < (MAX_THREAD)) || (id >= STORY_THREAD && id < MAX_STORY_THREAD);
|
||||
}
|
||||
|
||||
public static int getNotificationIdForUnreadReminder(long threadId) {
|
||||
return UNREAD_REMINDER + (int) threadId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Manages enqueueing [UnreadReminderJob] when a thread has an unread message/call that exceeds the reminder threshold.
|
||||
*/
|
||||
class UnreadReminderManager(
|
||||
val application: Application
|
||||
) : TimedEventManager<UnreadReminderManager.Event>(application, "UnreadReminderManager") {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(UnreadReminderManager::class.java)
|
||||
private val reminderThreshold: Long
|
||||
get() = RemoteConfig.unreadReminderIntervalSeconds.seconds.inWholeMilliseconds
|
||||
|
||||
val MAX_UNREAD_MESSAGE_AGE = 14.days
|
||||
}
|
||||
|
||||
init {
|
||||
scheduleIfNecessary()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun getNextClosestEvent(): Event? {
|
||||
val messageThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.MESSAGES, reminderThreshold)
|
||||
val callThreadIds = SignalDatabase.threads.getMutedThreadIds(ReminderType.CALLS, reminderThreshold)
|
||||
|
||||
val (messageThreadId, messageTimestamp) = SignalDatabase.messages.getOldestUnreadMessage(messageThreadIds)
|
||||
val (callThreadId, callTimestamp) = SignalDatabase.calls.getOldestUnreadCall(callThreadIds)
|
||||
|
||||
return if (messageThreadId == -1L && callThreadId == -1L) {
|
||||
Log.i(TAG, "No existing unread message or calls from a qualifying thread.")
|
||||
cancelAlarm(application, UnreadReminderAlarm::class.java)
|
||||
null
|
||||
} else if (messageTimestamp < callTimestamp) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun executeEvent(event: Event) {
|
||||
Log.i(TAG, "Executing event $event")
|
||||
val lastReminderTime = SignalDatabase.threads.getUnreadReminderTime(event.threadId)
|
||||
SignalDatabase.threads.setUnreadReminderTime(event.threadId, System.currentTimeMillis())
|
||||
UnreadReminderJob.enqueue(event.threadId, lastReminderTime)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun getDelayForEvent(event: Event): Long = event.delay
|
||||
|
||||
@WorkerThread
|
||||
override fun scheduleAlarm(application: Application, event: Event, delay: Long) {
|
||||
setAlarm(application, delay, UnreadReminderAlarm::class.java)
|
||||
}
|
||||
|
||||
data class Event(val delay: Long, val threadId: Long)
|
||||
|
||||
class UnreadReminderAlarm : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(UnreadReminderAlarm::class.java)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
Log.d(TAG, "onReceive()")
|
||||
AppDependencies.unreadReminderManager.scheduleIfNecessary()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,3 +296,8 @@ message MultiDeviceAttachmentBackfillRequestJobData {
|
||||
message BackupTierDowngradeCheckJobData {
|
||||
optional uint64 remoteBackupTier = 1;
|
||||
}
|
||||
|
||||
message UnreadReminderJobData {
|
||||
uint64 threadId = 1;
|
||||
uint64 lastReminderTime = 2;
|
||||
}
|
||||
|
||||
@@ -3391,6 +3391,8 @@
|
||||
<string name="NotificationChannel_additional_message_notifications">Additional message notifications</string>
|
||||
<!-- Notification channel name for notifications sent when a device has been linked -->
|
||||
<string name="NotificationChannel_new_linked_device">New linked device</string>
|
||||
<!-- Notification channel name for unread reminder notifications -->
|
||||
<string name="NotificationChannel_unread_reminders">Unread reminders</string>
|
||||
|
||||
<!-- QuickResponseService -->
|
||||
<string name="QuickResponseService_quick_response_unavailable_when_Signal_is_locked">Quick response unavailable when Signal is locked!</string>
|
||||
|
||||
+5
-5
@@ -32,7 +32,7 @@ class CallTableTest_unreadForReminder {
|
||||
insertMissedCall(caller, time = 1000)
|
||||
insertMissedCall(caller, time = 1001)
|
||||
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(listOf(threadId))
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1001)
|
||||
|
||||
assertThat(count).isEqualTo(2)
|
||||
assertThat(authors).isEqualTo(listOf(caller))
|
||||
@@ -45,7 +45,7 @@ class CallTableTest_unreadForReminder {
|
||||
|
||||
insertCall(caller, time = 1000, event = CallTable.Event.MISSED_NOTIFICATION_PROFILE)
|
||||
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(listOf(threadId))
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1001)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(caller))
|
||||
@@ -59,7 +59,7 @@ class CallTableTest_unreadForReminder {
|
||||
insertCall(caller, time = 1000, event = CallTable.Event.ACCEPTED)
|
||||
insertMissedCall(caller, time = 1001)
|
||||
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(listOf(threadId))
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(caller))
|
||||
@@ -74,7 +74,7 @@ class CallTableTest_unreadForReminder {
|
||||
calls.markAllCallEventsRead(timestamp = 1000)
|
||||
insertMissedCall(caller, time = 1001)
|
||||
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(listOf(threadId))
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(threadId, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(caller))
|
||||
@@ -90,7 +90,7 @@ class CallTableTest_unreadForReminder {
|
||||
insertMissedCall(included, time = 1000)
|
||||
insertMissedCall(excluded, time = 1000)
|
||||
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(listOf(includedThreadId))
|
||||
val (count, authors) = calls.getUnreadCallsForReminderNotification(includedThreadId, 1001)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(included))
|
||||
|
||||
+19
-6
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
@@ -34,7 +35,7 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(threadId, sender, time = 1000)
|
||||
insertIncoming(threadId, sender, time = 1001)
|
||||
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(listOf(threadId), ReminderType.MESSAGES)
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.MESSAGES, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(2)
|
||||
assertThat(authors).isEqualTo(listOf(sender))
|
||||
@@ -48,7 +49,7 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(threadId, sender, time = 1000)
|
||||
insertIncoming(threadId, sender, time = 1001, mentions = listOf(Mention(recipients.self, 0, 1)))
|
||||
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(listOf(threadId), ReminderType.MENTIONS)
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.MENTIONS, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(sender))
|
||||
@@ -72,7 +73,7 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(threadId, sender, time = 1000)
|
||||
insertIncoming(threadId, sender, time = 1001, quote = quote)
|
||||
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(listOf(threadId), ReminderType.REPLIES)
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.REPLIES, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(sender))
|
||||
@@ -87,7 +88,7 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(threadId, sender, time = 1001)
|
||||
markRead(read)
|
||||
|
||||
val (count, _) = messages.getUnreadContentForReminderNotification(listOf(threadId), ReminderType.MESSAGES)
|
||||
val (count, _) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.MESSAGES, 1002)
|
||||
assertThat(count).isEqualTo(1)
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(includedThreadId, included, time = 1000)
|
||||
insertIncoming(excludedThreadId, excluded, time = 1000)
|
||||
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(listOf(includedThreadId), ReminderType.MESSAGES)
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(includedThreadId, ReminderType.MESSAGES, 1002)
|
||||
|
||||
assertThat(count).isEqualTo(1)
|
||||
assertThat(authors).isEqualTo(listOf(included))
|
||||
@@ -122,12 +123,24 @@ class MessageTableTest_unreadForReminder {
|
||||
insertIncoming(threadId, c, time = 1003)
|
||||
insertIncoming(threadId, d, time = 1004)
|
||||
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(listOf(threadId), ReminderType.MESSAGES)
|
||||
val (count, authors) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.MESSAGES, 1005)
|
||||
|
||||
assertThat(count).isEqualTo(5)
|
||||
assertThat(authors).isEqualTo(listOf(d, c, b))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages more than two weeks old are excluded`() {
|
||||
val sender = recipients.createRecipient("Erin Reader")
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(sender))
|
||||
|
||||
insertIncoming(threadId, sender, time = 1000)
|
||||
insertIncoming(threadId, sender, time = 1001)
|
||||
|
||||
val (count, _) = messages.getUnreadContentForReminderNotification(threadId, ReminderType.MESSAGES, 1000 + 14.days.inWholeMilliseconds)
|
||||
assertThat(count).isEqualTo(1)
|
||||
}
|
||||
|
||||
private fun insertIncoming(threadId: Long, from: RecipientId, time: Long, quote: QuoteModel? = null, mentions: List<Mention> = emptyList()): Long {
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
|
||||
+31
-48
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.Reminder
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testutil.RecipientTestRule
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
@@ -31,6 +32,7 @@ class ThreadTableTest_mutedThreadIds {
|
||||
fun setUp() {
|
||||
contactId = recipients.createRecipient("Alice Android")
|
||||
threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(contactId))
|
||||
SignalDatabase.threads.markAsActiveEarly(threadId)
|
||||
SignalDatabase.recipients.setMuted(contactId, Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
@@ -41,14 +43,15 @@ class ThreadTableTest_mutedThreadIds {
|
||||
every { recipients.signalStore.settings.allowRepliesWhileMuted } returns replies
|
||||
}
|
||||
|
||||
private fun isMutedFor(reminderType: ReminderType): Boolean {
|
||||
return threadId in SignalDatabase.threads.getMutedThreadIds(reminderType)
|
||||
private fun isMutedFor(reminderType: ReminderType, threshold: Long = 0): Boolean {
|
||||
return threadId in SignalDatabase.threads.getMutedThreadIds(reminderType, threshold)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `allow-by-default plus system-default recipient setting includes the thread`() {
|
||||
globalDefaults(unreadReminder = true)
|
||||
SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT)
|
||||
SignalDatabase.threads.incrementUnread(threadId, 1, 1)
|
||||
assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue()
|
||||
}
|
||||
|
||||
@@ -63,6 +66,7 @@ class ThreadTableTest_mutedThreadIds {
|
||||
fun `allow-by-default plus an explicit always-allow includes the thread`() {
|
||||
globalDefaults(unreadReminder = true)
|
||||
SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
SignalDatabase.threads.incrementUnread(threadId, 1, 1)
|
||||
assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue()
|
||||
}
|
||||
|
||||
@@ -84,6 +88,7 @@ class ThreadTableTest_mutedThreadIds {
|
||||
fun `always-allow-required plus an explicit always-allow includes the thread`() {
|
||||
globalDefaults(unreadReminder = false)
|
||||
SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
SignalDatabase.threads.incrementUnread(threadId, 1, 1)
|
||||
assertThat(isMutedFor(ReminderType.MESSAGES)).isTrue()
|
||||
}
|
||||
|
||||
@@ -95,6 +100,30 @@ class ThreadTableTest_mutedThreadIds {
|
||||
assertThat(isMutedFor(ReminderType.MESSAGES)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a thread reminded within the last three days is excluded`() {
|
||||
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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a thread reminded more than three days ago is included again`() {
|
||||
globalDefaults(unreadReminder = true)
|
||||
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()
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an archived thread is excluded even when muted and always-allowed`() {
|
||||
globalDefaults(unreadReminder = true)
|
||||
@@ -126,50 +155,4 @@ class ThreadTableTest_mutedThreadIds {
|
||||
SignalDatabase.recipients.setCallNotificationSetting(contactId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT)
|
||||
assertThat(isMutedFor(ReminderType.CALLS)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mentions are excluded for a 1-to-1 thread no matter the notification settings`() {
|
||||
globalDefaults(unreadReminder = true, mentions = true)
|
||||
SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
SignalDatabase.recipients.setMentionSetting(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
assertThat(isMutedFor(ReminderType.MENTIONS)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mentions in a muted GV2 group thread follow the same allow-by-default vs always-allow rule`() {
|
||||
val group = recipients.createGroup()
|
||||
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(group.recipientId))
|
||||
SignalDatabase.recipients.setMuted(group.recipientId, Long.MAX_VALUE)
|
||||
SignalDatabase.recipients.setUnreadReminder(group.recipientId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
globalDefaults(unreadReminder = true, mentions = false)
|
||||
|
||||
SignalDatabase.recipients.setMentionSetting(group.recipientId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT)
|
||||
assertThat(groupThreadId in SignalDatabase.threads.getMutedThreadIds(ReminderType.MENTIONS)).isFalse()
|
||||
|
||||
SignalDatabase.recipients.setMentionSetting(group.recipientId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
assertThat(groupThreadId in SignalDatabase.threads.getMutedThreadIds(ReminderType.MENTIONS)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replies are excluded for a 1-to-1 thread no matter the notification settings`() {
|
||||
globalDefaults(unreadReminder = true, replies = true)
|
||||
SignalDatabase.recipients.setUnreadReminder(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
SignalDatabase.recipients.setReplyNotificationSetting(contactId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
assertThat(isMutedFor(ReminderType.REPLIES)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replies in a muted GV2 group thread follow the same allow-by-default vs always-allow rule`() {
|
||||
val group = recipients.createGroup()
|
||||
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(group.recipientId))
|
||||
SignalDatabase.recipients.setMuted(group.recipientId, Long.MAX_VALUE)
|
||||
SignalDatabase.recipients.setUnreadReminder(group.recipientId, RecipientTable.NotificationSetting.ALWAYS_NOTIFY)
|
||||
globalDefaults(unreadReminder = true, replies = true)
|
||||
|
||||
SignalDatabase.recipients.setReplyNotificationSetting(group.recipientId, RecipientTable.NotificationSetting.DO_NOT_NOTIFY)
|
||||
assertThat(groupThreadId in SignalDatabase.threads.getMutedThreadIds(ReminderType.REPLIES)).isFalse()
|
||||
|
||||
SignalDatabase.recipients.setReplyNotificationSetting(group.recipientId, RecipientTable.NotificationSetting.SYSTEM_DEFAULT)
|
||||
assertThat(groupThreadId in SignalDatabase.threads.getMutedThreadIds(ReminderType.REPLIES)).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.service.PendingRetryReceiptManager
|
||||
import org.thoughtcrime.securesms.service.PinnedMessageManager
|
||||
import org.thoughtcrime.securesms.service.ScheduledMessageManager
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager
|
||||
import org.thoughtcrime.securesms.service.UnreadReminderManager
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager
|
||||
import org.thoughtcrime.securesms.shakereport.ShakeToReport
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache
|
||||
@@ -271,6 +272,10 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
|
||||
override fun provideUnreadReminderManager(): UnreadReminderManager {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
|
||||
override fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user