Add story distribution list deduplication handling.
This commit is contained in:
parent
ba394e1021
commit
2f5cb5f090
18 changed files with 565 additions and 57 deletions
|
@ -0,0 +1,55 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
*/
|
||||
object MmsHelper {
|
||||
|
||||
fun insert(
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
threadId: Long = 1,
|
||||
storyType: StoryType = StoryType.NONE
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet()
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
threadId = threadId,
|
||||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: OutgoingMediaMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.containsInAnyOrder
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendsDatabaseTest {
|
||||
|
||||
private lateinit var recipients1to10: List<RecipientId>
|
||||
private lateinit var recipients11to20: List<RecipientId>
|
||||
private lateinit var recipients6to15: List<RecipientId>
|
||||
private lateinit var recipients6to10: List<RecipientId>
|
||||
|
||||
private var messageId1: Long = 0
|
||||
private var messageId2: Long = 0
|
||||
private var messageId3: Long = 0
|
||||
|
||||
private lateinit var storySends: StorySendsDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
storySends = SignalDatabase.storySends
|
||||
|
||||
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
|
||||
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
|
||||
recipients1to10 = makeRecipients(10)
|
||||
recipients11to20 = makeRecipients(10)
|
||||
|
||||
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
||||
recipients6to10 = recipients1to10.takeLast(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapAll() {
|
||||
val recipient1 = recipients1to10.first()
|
||||
val recipient2 = recipients11to20.first()
|
||||
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(0))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(1))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
|
||||
|
||||
assertThat(recipientIdsForMessage3, hasSize(1))
|
||||
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||
storySends.insert(messageId1, recipients6to15, 100, true)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithReplies() {
|
||||
storySends.insert(messageId2, recipients1to10, 200, true)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithoutReplies() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true)
|
||||
|
||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||
|
||||
assertThat(message1OnlyRecipientCanReply, `is`(false))
|
||||
assertThat(message2RecipientCanReply, `is`(true))
|
||||
}
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -133,7 +133,7 @@ public class MmsDatabase extends MessageDatabase {
|
|||
static final String MESSAGE_RANGES = "ranges";
|
||||
|
||||
public static final String VIEW_ONCE = "reveal_duration";
|
||||
static final String STORY_TYPE = "is_story";
|
||||
public static final String STORY_TYPE = "is_story";
|
||||
static final String PARENT_STORY_ID = "parent_story_id";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
|
@ -930,9 +930,25 @@ public class MmsDatabase extends MessageDatabase {
|
|||
if (messageUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) {
|
||||
earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp);
|
||||
}
|
||||
|
||||
return messageUpdates;
|
||||
}
|
||||
|
||||
String columnName = receiptType.getColumnName();
|
||||
for (MessageId storyMessageId : SignalDatabase.storySends().getStoryMessagesFor(messageId)) {
|
||||
database.execSQL("UPDATE " + TABLE_NAME + " SET " +
|
||||
columnName + " = " + columnName + " + 1, " +
|
||||
RECEIPT_TIMESTAMP + " = CASE " +
|
||||
"WHEN " + columnName + " = 0 THEN MAX(" + RECEIPT_TIMESTAMP + ", ?) " +
|
||||
"ELSE " + RECEIPT_TIMESTAMP + " " +
|
||||
"END " +
|
||||
"WHERE " + ID + " = ?",
|
||||
SqlUtil.buildArgs(timestamp, storyMessageId.getId()));
|
||||
|
||||
SignalDatabase.groupReceipts().update(messageId.getRecipientId(), storyMessageId.getId(), receiptType.getGroupStatus(), timestamp);
|
||||
|
||||
messageUpdates.add(new MessageUpdate(-1, storyMessageId));
|
||||
}
|
||||
|
||||
return messageUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1879,6 +1895,15 @@ public class MmsDatabase extends MessageDatabase {
|
|||
|
||||
receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis());
|
||||
|
||||
for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) {
|
||||
receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
|
||||
}
|
||||
} else if (message.getRecipient().isDistributionList()) {
|
||||
GroupReceiptDatabase receiptDatabase = SignalDatabase.groupReceipts();
|
||||
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(message.getRecipient().requireDistributionListId());
|
||||
|
||||
receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis());
|
||||
|
||||
for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) {
|
||||
receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notification
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.storySends
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
|
@ -2707,6 +2708,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
|||
// DistributionLists
|
||||
distributionLists.remapRecipient(byE164, byAci)
|
||||
|
||||
// Story Sends
|
||||
storySends.remapRecipient(byE164, byAci)
|
||||
|
||||
// Recipient
|
||||
Log.w(TAG, "Deleting recipient $byE164", true)
|
||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))
|
||||
|
|
|
@ -70,6 +70,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
|
||||
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
|
||||
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
|
||||
val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.enableWriteAheadLogging()
|
||||
|
@ -103,6 +104,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||
db.execSQL(GroupCallRingDatabase.CREATE_TABLE)
|
||||
db.execSQL(ReactionDatabase.CREATE_TABLE)
|
||||
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
|
||||
db.execSQL(StorySendsDatabase.CREATE_TABLE)
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
||||
|
@ -125,6 +127,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS)
|
||||
db.execSQL(StorySendsDatabase.CREATE_INDEX)
|
||||
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||
|
@ -480,5 +483,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||
@get:JvmName("donationReceipts")
|
||||
val donationReceipts: DonationReceiptDatabase
|
||||
get() = instance!!.donationReceiptDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("storySends")
|
||||
val storySends: StorySendsDatabase
|
||||
get() = instance!!.storySendsDatabase
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to
|
||||
* show them as distinct items on the sending side, but as a single item on the receiving side. Basically, if Alice has two lists and Bob
|
||||
* is on both, Bob should always see a story for “Alice” and not know that Alice has him in multiple lists. And when Bob views the story,
|
||||
* Alice should update the UI to show a view in each list. To do this, we need to:
|
||||
* 1. Only send a single copy of each story to a given recipient, while
|
||||
* 2. Knowing which people would have gotten duplicate copies.
|
||||
*/
|
||||
class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "story_sends"
|
||||
const val ID = "_id"
|
||||
const val MESSAGE_ID = "message_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val SENT_TIMESTAMP = "sent_timestamp"
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
||||
$SENT_TIMESTAMP INTEGER NOT NULL,
|
||||
$ALLOWS_REPLIES INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
val CREATE_INDEX = """
|
||||
CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON $TABLE_NAME ($RECIPIENT_ID, $SENT_TIMESTAMP, $ALLOWS_REPLIES)
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean) {
|
||||
val db = writableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val insertValues: List<ContentValues> = recipientIds.map { id ->
|
||||
contentValuesOf(
|
||||
MESSAGE_ID to messageId,
|
||||
RECIPIENT_ID to id.serialize(),
|
||||
SENT_TIMESTAMP to sentTimestamp,
|
||||
ALLOWS_REPLIES to allowsReplies.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES), insertValues)
|
||||
.forEach { query -> db.execSQL(query.where, query.whereArgs) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecipientsToSendTo(messageId: Long, sentTimestamp: Long, allowsReplies: Boolean): List<RecipientId> {
|
||||
val recipientIds = mutableListOf<RecipientId>()
|
||||
|
||||
val query = """
|
||||
SELECT DISTINCT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE
|
||||
$MESSAGE_ID = $messageId
|
||||
AND $RECIPIENT_ID NOT IN (
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE
|
||||
$SENT_TIMESTAMP = $sentTimestamp
|
||||
AND $MESSAGE_ID < $messageId
|
||||
AND $ALLOWS_REPLIES >= ${allowsReplies.toInt()}
|
||||
)
|
||||
AND $RECIPIENT_ID NOT IN (
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE
|
||||
$SENT_TIMESTAMP = $sentTimestamp
|
||||
AND $MESSAGE_ID > $messageId
|
||||
AND $ALLOWS_REPLIES > ${allowsReplies.toInt()}
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
readableDatabase.rawQuery(query, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
recipientIds += RecipientId.from(cursor.requireLong(RECIPIENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
return recipientIds
|
||||
}
|
||||
|
||||
/**
|
||||
* The weirdness with remote deletes and stories is that just because you remote-delete a story to List A doesn’t mean you
|
||||
* send the delete to everyone on the list – some people have it through multiple lists.
|
||||
*
|
||||
* The general idea is to find all recipients for a story that still have a non-deleted copy of it.
|
||||
*/
|
||||
fun getRemoteDeleteRecipients(messageId: Long, sentTimestamp: Long): List<RecipientId> {
|
||||
val recipientIds = mutableListOf<RecipientId>()
|
||||
|
||||
val query = """
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE
|
||||
$MESSAGE_ID = $messageId
|
||||
AND $RECIPIENT_ID NOT IN (
|
||||
SELECT $RECIPIENT_ID
|
||||
FROM $TABLE_NAME
|
||||
WHERE $MESSAGE_ID != $messageId
|
||||
AND $SENT_TIMESTAMP = $sentTimestamp
|
||||
AND $MESSAGE_ID IN (
|
||||
SELECT ${MmsDatabase.ID}
|
||||
FROM ${MmsDatabase.TABLE_NAME}
|
||||
WHERE ${MmsDatabase.REMOTE_DELETED} = 0
|
||||
)
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
readableDatabase.rawQuery(query, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
recipientIds += RecipientId.from(cursor.requireLong(RECIPIENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
return recipientIds
|
||||
}
|
||||
|
||||
fun canReply(recipientId: RecipientId, sentTimestamp: Long): Boolean {
|
||||
readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf("1"),
|
||||
"$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ? AND $ALLOWS_REPLIES = ?",
|
||||
SqlUtil.buildArgs(recipientId, sentTimestamp, 1),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
return it.moveToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
fun getStoryMessagesFor(syncMessageId: MessageDatabase.SyncMessageId): Set<MessageId> {
|
||||
val messageIds = mutableSetOf<MessageId>()
|
||||
|
||||
readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(MESSAGE_ID),
|
||||
"$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ?",
|
||||
SqlUtil.buildArgs(syncMessageId.recipientId, syncMessageId.timetamp),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
messageIds += MessageId(cursor.requireLong(MESSAGE_ID), true)
|
||||
}
|
||||
}
|
||||
|
||||
return messageIds
|
||||
}
|
||||
|
||||
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
|
||||
val query = "$RECIPIENT_ID = ?"
|
||||
val args = SqlUtil.buildArgs(oldId)
|
||||
val values = contentValuesOf(RECIPIENT_ID to newId.serialize())
|
||||
|
||||
writableDatabase.update(TABLE_NAME, values, query, args)
|
||||
}
|
||||
}
|
|
@ -195,8 +195,9 @@ object SignalDatabaseMigrations {
|
|||
private const val ALLOW_STORY_REPLIES = 133
|
||||
private const val GROUP_STORIES = 134
|
||||
private const val MMS_COUNT_INDEX = 135
|
||||
private const val STORY_SENDS = 136
|
||||
|
||||
const val DATABASE_VERSION = 135
|
||||
const val DATABASE_VERSION = 136
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
@ -2486,6 +2487,22 @@ object SignalDatabaseMigrations {
|
|||
if (oldVersion < MMS_COUNT_INDEX) {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON mms (thread_id, date_received, is_story, parent_story_id)")
|
||||
}
|
||||
|
||||
if (oldVersion < STORY_SENDS) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE story_sends (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
sent_timestamp INTEGER NOT NULL,
|
||||
allows_replies INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -145,7 +145,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
List<Recipient> target;
|
||||
|
||||
if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).distinct().map(Recipient::resolved).toList();
|
||||
else target = Stream.of(Stories.getRecipientsToSendTo(listRecipient.requireDistributionListId(), messageId)).distinctBy(Recipient::getId).toList();
|
||||
else target = Stream.of(Stories.getRecipientsToSendTo(messageId, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies())).distinctBy(Recipient::getId).toList();
|
||||
|
||||
List<SendMessageResult> results = deliver(message, target);
|
||||
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -72,12 +73,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
|
||||
List<RecipientId> recipients;
|
||||
if (conversationRecipient.isDistributionList()) {
|
||||
DistributionListId distributionListId = conversationRecipient.requireDistributionListId();
|
||||
|
||||
recipients = Stories.getRecipientsToSendTo(distributionListId, messageId)
|
||||
.stream()
|
||||
.map(Recipient::getId)
|
||||
.collect(Collectors.toList());
|
||||
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
|
||||
} else {
|
||||
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
||||
: Stream.of(conversationRecipient.getId()).toList();
|
||||
|
|
|
@ -201,6 +201,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
private fun sendMessages(contacts: List<RecipientSearchKey>, body: String, preUploadResults: Collection<PreUploadResult>, mentions: List<Mention>, isViewOnce: Boolean) {
|
||||
val broadcastMessages: MutableList<OutgoingSecureMediaMessage> = ArrayList(contacts.size)
|
||||
val storyMessages: MutableMap<PreUploadResult, MutableList<OutgoingSecureMediaMessage>> = mutableMapOf()
|
||||
val distributionListSentTimestamps: MutableMap<PreUploadResult, Long> = mutableMapOf()
|
||||
|
||||
for (contact in contacts) {
|
||||
val recipient = Recipient.resolved(contact.recipientId)
|
||||
|
@ -220,7 +221,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
if (recipient.isDistributionList) distributionListSentTimestamps.getOrPut(preUploadResults.first()) { System.currentTimeMillis() } else System.currentTimeMillis(),
|
||||
-1,
|
||||
TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
|
||||
isViewOnce,
|
||||
|
@ -239,7 +240,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
if (isStory && preUploadResults.size > 1) {
|
||||
preUploadResults.forEach {
|
||||
val list = storyMessages[it] ?: mutableListOf()
|
||||
list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(System.currentTimeMillis()))
|
||||
list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(if (recipient.isDistributionList) distributionListSentTimestamps.getOrPut(it) { System.currentTimeMillis() } else System.currentTimeMillis()))
|
||||
storyMessages[it] = list
|
||||
|
||||
// XXX We must do this to avoid sending out messages to the same recipient with the same
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.thoughtcrime.securesms.mediasend.v2.text.send
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
|
@ -44,6 +43,7 @@ class TextStoryPostSendRepository {
|
|||
private fun performSend(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
|
||||
return Single.fromCallable {
|
||||
val messages: MutableList<OutgoingSecureMediaMessage> = mutableListOf()
|
||||
val distributionListSentTimestamp = System.currentTimeMillis()
|
||||
|
||||
for (contact in contactSearchKey) {
|
||||
val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get())
|
||||
|
@ -63,7 +63,7 @@ class TextStoryPostSendRepository {
|
|||
recipient,
|
||||
serializeTextStoryState(textStoryPostCreationState),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
if (recipient.isDistributionList) distributionListSentTimestamp else System.currentTimeMillis(),
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
|
@ -83,9 +83,9 @@ class TextStoryPostSendRepository {
|
|||
ThreadUtil.sleep(5)
|
||||
}
|
||||
|
||||
messages.map { Stories.sendIndividualStory(it) }
|
||||
Stories.sendTextStories(messages)
|
||||
}.flatMap { messages ->
|
||||
Completable.concat(messages).toSingleDefault<TextStoryPostSendResult>(TextStoryPostSendResult.Success)
|
||||
messages.toSingleDefault<TextStoryPostSendResult>(TextStoryPostSendResult.Success)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1564,16 +1564,14 @@ public final class MessageContentProcessor {
|
|||
|
||||
if (message.getGroupContext().isPresent()) {
|
||||
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
|
||||
} else {
|
||||
} else if (SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) {
|
||||
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
|
||||
|
||||
if (!story.getStoryType().isStoryWithReplies()) {
|
||||
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
|
||||
return;
|
||||
}
|
||||
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, "", false, story.getSlideDeck().asAttachments(), Collections.emptyList());
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, message.getBody().orElse(""), false, story.getSlideDeck().asAttachments(), Collections.emptyList());
|
||||
} else {
|
||||
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
|
||||
return;
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
warn(content.getTimestamp(), "Couldn't find story for reply.", e);
|
||||
|
|
|
@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -304,14 +305,9 @@ public class MessageSender {
|
|||
OutgoingSecureMediaMessage message = messages.get(i);
|
||||
Recipient recipient = message.getRecipient();
|
||||
|
||||
if (isLocalSelfSend(context, recipient, false)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (recipient.isPushGroup()) {
|
||||
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), null, true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
} else if (recipient.isDistributionList()) {
|
||||
jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
} else {
|
||||
jobManager.add(new PushMediaSendJob(messageId, recipient, true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
if (recipient.isDistributionList()) {
|
||||
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
||||
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,9 +315,25 @@ public class MessageSender {
|
|||
mmsDatabase.setTransactionSuccessful();
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Failed to send messages.", e);
|
||||
return;
|
||||
} finally {
|
||||
mmsDatabase.endTransaction();
|
||||
}
|
||||
|
||||
for (int i = 0; i < messageIds.size(); i++) {
|
||||
long messageId = messageIds.get(i);
|
||||
Recipient recipient = messages.get(i).getRecipient();
|
||||
|
||||
if (isLocalSelfSend(context, recipient, false)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (recipient.isPushGroup()) {
|
||||
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), null, true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
} else if (recipient.isDistributionList()) {
|
||||
jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
} else {
|
||||
jobManager.add(new PushMediaSendJob(messageId, recipient, true), messageDependsOnIds, recipient.getId().toQueueKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,13 +5,11 @@ import androidx.fragment.app.FragmentManager
|
|||
import io.reactivex.rxjava3.core.Completable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
|
@ -40,29 +38,16 @@ object Stories {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
fun sendIndividualStory(message: OutgoingMediaMessage): Completable {
|
||||
fun sendTextStories(messages: List<OutgoingSecureMediaMessage>): Completable {
|
||||
return Completable.create { emitter ->
|
||||
MessageSender.send(
|
||||
ApplicationDependencies.getApplication(),
|
||||
message,
|
||||
-1L,
|
||||
false,
|
||||
null
|
||||
) {
|
||||
emitter.onComplete()
|
||||
}
|
||||
MessageSender.sendMediaBroadcast(ApplicationDependencies.getApplication(), messages, listOf(), listOf())
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getRecipientsToSendTo(distributionListId: DistributionListId, messageId: Long): List<Recipient> {
|
||||
val destinations: List<GroupReceiptInfo> = SignalDatabase.groupReceipts.getGroupReceiptInfo(messageId)
|
||||
|
||||
val recipientIds: List<RecipientId> = if (destinations.isNotEmpty()) {
|
||||
destinations.map(GroupReceiptInfo::getRecipientId)
|
||||
} else {
|
||||
SignalDatabase.distributionLists.getMembers(distributionListId)
|
||||
}
|
||||
fun getRecipientsToSendTo(messageId: Long, sentTimestamp: Long, allowsReplies: Boolean): List<Recipient> {
|
||||
val recipientIds: List<RecipientId> = SignalDatabase.storySends.getRecipientsToSendTo(messageId, sentTimestamp, allowsReplies)
|
||||
|
||||
return RecipientUtil.getEligibleForSending(recipientIds.map(Recipient::resolved))
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ object MyStoriesItem {
|
|||
|
||||
viewCount.text = context.resources.getQuantityString(
|
||||
R.plurals.MyStories__d_views,
|
||||
model.distributionStory.messageRecord.readReceiptCount,
|
||||
model.distributionStory.messageRecord.readReceiptCount
|
||||
model.distributionStory.messageRecord.viewedReceiptCount,
|
||||
model.distributionStory.messageRecord.viewedReceiptCount
|
||||
)
|
||||
|
||||
if (STATUS_CHANGE in payload) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.signal.spinner.Spinner.DatabaseConfig
|
|||
import org.thoughtcrime.securesms.database.DatabaseMonitor
|
||||
import org.thoughtcrime.securesms.database.GV2Transformer
|
||||
import org.thoughtcrime.securesms.database.GV2UpdateTransformer
|
||||
import org.thoughtcrime.securesms.database.IsStoryTransformer
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
|
@ -39,7 +40,7 @@ class SpinnerApplicationContext : ApplicationContext() {
|
|||
linkedMapOf(
|
||||
"signal" to DatabaseConfig(
|
||||
db = SignalDatabase.rawDatabase,
|
||||
columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer)
|
||||
columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer)
|
||||
),
|
||||
"jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase),
|
||||
"keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase),
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.thoughtcrime.securesms.database.model.StoryType.Companion.fromCode
|
||||
|
||||
object IsStoryTransformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return columnName == MmsDatabase.STORY_TYPE && (tableName == null || tableName == MmsDatabase.TABLE_NAME)
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
||||
val storyType = fromCode(cursor.requireInt(MmsDatabase.STORY_TYPE))
|
||||
return "${cursor.requireInt(MmsDatabase.STORY_TYPE)}<br><br>$storyType"
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import com.google.android.mms.pdu_alt.PduHeaders
|
|||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
|
@ -15,6 +16,7 @@ object TestMms {
|
|||
fun insert(
|
||||
db: SQLiteDatabase,
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
recipientId: RecipientId = Recipient.UNKNOWN.id,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
|
@ -51,6 +53,7 @@ object TestMms {
|
|||
return insert(
|
||||
db = db,
|
||||
message = message,
|
||||
recipientId = recipientId,
|
||||
body = body,
|
||||
type = type,
|
||||
unread = unread,
|
||||
|
@ -63,6 +66,7 @@ object TestMms {
|
|||
fun insert(
|
||||
db: SQLiteDatabase,
|
||||
message: OutgoingMediaMessage,
|
||||
recipientId: RecipientId = message.recipient.id,
|
||||
body: String = message.body,
|
||||
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
|
||||
unread: Boolean = false,
|
||||
|
@ -81,7 +85,7 @@ object TestMms {
|
|||
put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
|
||||
put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
|
||||
put(MmsDatabase.VIEW_ONCE, message.isViewOnce)
|
||||
put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
|
||||
put(MmsSmsColumns.RECIPIENT_ID, recipientId.serialize())
|
||||
put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
|
||||
put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
|
||||
put(MmsSmsColumns.VIEWED_RECEIPT_COUNT, if (viewed) 1 else 0)
|
||||
|
@ -94,4 +98,17 @@ object TestMms {
|
|||
|
||||
return db.insert(MmsDatabase.TABLE_NAME, null, contentValues)
|
||||
}
|
||||
|
||||
fun markAsRemoteDelete(db: SQLiteDatabase, messageId: Long) {
|
||||
val values = ContentValues()
|
||||
values.put(MmsSmsColumns.REMOTE_DELETED, 1)
|
||||
values.putNull(MmsSmsColumns.BODY)
|
||||
values.putNull(MmsDatabase.QUOTE_BODY)
|
||||
values.putNull(MmsDatabase.QUOTE_AUTHOR)
|
||||
values.putNull(MmsDatabase.QUOTE_ATTACHMENT)
|
||||
values.putNull(MmsDatabase.QUOTE_ID)
|
||||
values.putNull(MmsDatabase.LINK_PREVIEWS)
|
||||
values.putNull(MmsDatabase.SHARED_CONTACTS)
|
||||
db.update(MmsDatabase.TABLE_NAME, values, Database.ID_WHERE, arrayOf(messageId.toString()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue