Add support for versioned expiration timers.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
parent
4152294b57
commit
1f196f74ff
43 changed files with 392 additions and 139 deletions
|
@ -77,7 +77,7 @@ class EditMessageSyncProcessorTest {
|
|||
.build()
|
||||
).build()
|
||||
).build()
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0, content.dataMessage?.expireTimerVersion ?: 1)
|
||||
val syncTextMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||
content = syncContent,
|
||||
|
@ -112,7 +112,7 @@ class EditMessageSyncProcessorTest {
|
|||
|
||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000, content.dataMessage?.expireTimerVersion ?: 1)
|
||||
val originalTextMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = originalTimestamp,
|
||||
|
|
|
@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
|||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
|
|
|
@ -96,6 +96,7 @@ class ConversationElementGenerator {
|
|||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
|
|
|
@ -11,7 +11,8 @@ object AppCapabilities {
|
|||
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
|
||||
return AccountAttributes.Capabilities(
|
||||
storage = storageCapable,
|
||||
deleteSync = true
|
||||
deleteSync = true,
|
||||
expireTimerVersion = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.ExpirationTimerUtil
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG: String = Log.tag(ExpireTimerSettingsRepository::class.java)
|
||||
|
@ -38,8 +39,8 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
|||
consumer.invoke(Result.failure(e))
|
||||
}
|
||||
} else {
|
||||
SignalDatabase.recipients.setExpireMessages(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingMessage.expirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
val expireTimerVersion = ExpirationTimerUtil.setExpirationTimer(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingMessage.expirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L, expireTimerVersion)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), MessageSender.SendType.SIGNAL, null, null)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
}
|
||||
|
|
|
@ -341,8 +341,9 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
|||
|
||||
return if (capabilities != null) {
|
||||
TextUtils.concat(
|
||||
colorize("DeleteSync", capabilities.deleteSync),
|
||||
", ",
|
||||
colorize("DeleteSync", capabilities.deleteSync)
|
||||
colorize("Expire Timer Version", capabilities.versionedExpirationTimer)
|
||||
)
|
||||
} else {
|
||||
"Recipient not found!"
|
||||
|
|
|
@ -716,7 +716,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
if (groupState?.disappearingMessagesTimer != null) {
|
||||
recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer!!.duration)
|
||||
recipients.setExpireMessagesForGroup(groupRecipientId, groupState.disappearingMessagesTimer!!.duration)
|
||||
}
|
||||
|
||||
if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {
|
||||
|
@ -843,7 +843,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
if (decryptedGroup.disappearingMessagesTimer != null) {
|
||||
recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer!!.duration)
|
||||
recipients.setExpireMessagesForGroup(groupRecipientId, decryptedGroup.disappearingMessagesTimer!!.duration)
|
||||
}
|
||||
|
||||
if (groupId.isMms || Recipient.resolved(groupRecipientId).isProfileSharing) {
|
||||
|
|
|
@ -169,6 +169,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
const val SMS_SUBSCRIPTION_ID = "subscription_id"
|
||||
const val EXPIRES_IN = "expires_in"
|
||||
const val EXPIRE_STARTED = "expire_started"
|
||||
const val EXPIRE_TIMER_VERSION = "expire_timer_version"
|
||||
const val NOTIFIED = "notified"
|
||||
const val NOTIFIED_TIMESTAMP = "notified_timestamp"
|
||||
const val UNIDENTIFIED = "unidentified"
|
||||
|
@ -264,7 +265,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
$LATEST_REVISION_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
|
||||
$ORIGINAL_MESSAGE_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
|
||||
$REVISION_NUMBER INTEGER DEFAULT 0,
|
||||
$MESSAGE_EXTRAS BLOB DEFAULT NULL
|
||||
$MESSAGE_EXTRAS BLOB DEFAULT NULL,
|
||||
$EXPIRE_TIMER_VERSION INTEGER DEFAULT 1 NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
@ -321,6 +323,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
SMS_SUBSCRIPTION_ID,
|
||||
EXPIRES_IN,
|
||||
EXPIRE_STARTED,
|
||||
EXPIRE_TIMER_VERSION,
|
||||
NOTIFIED,
|
||||
QUOTE_ID,
|
||||
QUOTE_AUTHOR,
|
||||
|
@ -2404,6 +2407,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
val timestamp = cursor.requireLong(DATE_SENT)
|
||||
val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
|
||||
val expiresIn = cursor.requireLong(EXPIRES_IN)
|
||||
val expireTimerVersion = cursor.requireInt(EXPIRE_TIMER_VERSION)
|
||||
val viewOnce = cursor.requireLong(VIEW_ONCE) == 1L
|
||||
val threadId = cursor.requireLong(THREAD_ID)
|
||||
val threadRecipient = Recipient.resolved(threads.getRecipientIdForThreadId(threadId)!!)
|
||||
|
@ -2480,7 +2484,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
OutgoingMessage.expirationUpdateMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn
|
||||
expiresIn = expiresIn,
|
||||
expireTimerVersion = expireTimerVersion
|
||||
)
|
||||
} else if (MessageTypes.isPaymentsNotification(outboxType)) {
|
||||
OutgoingMessage.paymentNotificationMessage(
|
||||
|
@ -2539,6 +2544,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
attachments = attachments,
|
||||
timestamp = timestamp,
|
||||
expiresIn = expiresIn,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
storyType = storyType,
|
||||
|
@ -2971,6 +2977,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
contentValues.put(DATE_RECEIVED, editedMessage?.dateReceived ?: System.currentTimeMillis())
|
||||
contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId)
|
||||
contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn)
|
||||
contentValues.put(EXPIRE_TIMER_VERSION, editedMessage?.expireTimerVersion ?: message.expireTimerVersion)
|
||||
contentValues.put(VIEW_ONCE, message.isViewOnce)
|
||||
contentValues.put(FROM_RECIPIENT_ID, Recipient.self().id.serialize())
|
||||
contentValues.put(FROM_DEVICE_ID, SignalStore.account.deviceId)
|
||||
|
@ -5214,6 +5221,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
|
||||
val expiresIn = cursor.requireLong(EXPIRES_IN)
|
||||
val expireStarted = cursor.requireLong(EXPIRE_STARTED)
|
||||
val expireTimerVersion = cursor.requireInt(EXPIRE_TIMER_VERSION)
|
||||
val unidentified = cursor.requireBoolean(UNIDENTIFIED)
|
||||
val isViewOnce = cursor.requireBoolean(VIEW_ONCE)
|
||||
val remoteDelete = cursor.requireBoolean(REMOTE_DELETED)
|
||||
|
@ -5296,6 +5304,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
subscriptionId,
|
||||
expiresIn,
|
||||
expireStarted,
|
||||
expireTimerVersion,
|
||||
isViewOnce,
|
||||
hasReadReceipt,
|
||||
quote,
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.signal.core.util.orNull
|
|||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSet
|
||||
import org.signal.core.util.readToSingleBoolean
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.readToSingleLong
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
|
@ -166,6 +167,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
const val CALL_VIBRATE = "call_vibrate"
|
||||
const val MUTE_UNTIL = "mute_until"
|
||||
const val MESSAGE_EXPIRATION_TIME = "message_expiration_time"
|
||||
const val MESSAGE_EXPIRATION_TIME_VERSION = "message_expiration_time_version"
|
||||
const val SEALED_SENDER_MODE = "sealed_sender_mode"
|
||||
const val STORAGE_SERVICE_ID = "storage_service_id"
|
||||
const val STORAGE_SERVICE_PROTO = "storage_service_proto"
|
||||
|
@ -263,7 +265,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
$NICKNAME_GIVEN_NAME TEXT DEFAULT NULL,
|
||||
$NICKNAME_FAMILY_NAME TEXT DEFAULT NULL,
|
||||
$NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
|
||||
$NOTE TEXT DEFAULT NULL
|
||||
$NOTE TEXT DEFAULT NULL,
|
||||
$MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
@ -307,6 +310,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
CALL_VIBRATE,
|
||||
MUTE_UNTIL,
|
||||
MESSAGE_EXPIRATION_TIME,
|
||||
MESSAGE_EXPIRATION_TIME_VERSION,
|
||||
SEALED_SENDER_MODE,
|
||||
STORAGE_SERVICE_ID,
|
||||
MENTION_SETTING,
|
||||
|
@ -410,6 +414,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long {
|
||||
var value: Long = 0
|
||||
value = Bitmask.update(value, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isDeleteSync).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isVersionedExpirationTimer).serialize().toLong())
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
@ -1473,7 +1478,27 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun setExpireMessages(id: RecipientId, expiration: Int) {
|
||||
fun setExpireMessagesAndIncrementVersion(id: RecipientId, expiration: Int): Int {
|
||||
val version = writableDatabase.rawQuery(
|
||||
"""
|
||||
UPDATE $TABLE_NAME
|
||||
SET $MESSAGE_EXPIRATION_TIME = $expiration,
|
||||
$MESSAGE_EXPIRATION_TIME_VERSION = MIN($MESSAGE_EXPIRATION_TIME_VERSION + 1, ${Int.MAX_VALUE})
|
||||
WHERE $ID = ${id.serialize()}
|
||||
RETURNING $MESSAGE_EXPIRATION_TIME_VERSION
|
||||
""",
|
||||
null
|
||||
).readToSingleInt(defaultValue = 1)
|
||||
|
||||
AppDependencies.databaseObserver.notifyRecipientChanged(id)
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expiration timer without incrementing the version. Will eventually be removed once everyone has the ability to understand expireTimerVersions.
|
||||
*/
|
||||
fun setExpireMessagesWithoutIncrementingVersion(id: RecipientId, expiration: Int) {
|
||||
val values = ContentValues(1).apply {
|
||||
put(MESSAGE_EXPIRATION_TIME, expiration)
|
||||
}
|
||||
|
@ -1482,6 +1507,23 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups do not have an expireTimerVersion, and therefore we do not need to provide them.
|
||||
*/
|
||||
fun setExpireMessagesForGroup(id: RecipientId, expiration: Int) {
|
||||
setExpireMessages(id, expiration, 1)
|
||||
}
|
||||
|
||||
fun setExpireMessages(id: RecipientId, expiration: Int, expirationVersion: Int) {
|
||||
val values = contentValuesOf(
|
||||
MESSAGE_EXPIRATION_TIME to expiration,
|
||||
MESSAGE_EXPIRATION_TIME_VERSION to expirationVersion
|
||||
)
|
||||
if (update(id, values)) {
|
||||
AppDependencies.databaseObserver.notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSealedSenderAccessMode(id: RecipientId, sealedSenderAccessMode: SealedSenderAccessMode) {
|
||||
val values = ContentValues(1).apply {
|
||||
put(SEALED_SENDER_MODE, sealedSenderAccessMode.mode)
|
||||
|
@ -3988,6 +4030,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
AVATAR_COLOR to primaryRecord.avatarColor.serialize(),
|
||||
CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null),
|
||||
MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages,
|
||||
MESSAGE_EXPIRATION_TIME_VERSION to max(primaryRecord.expireTimerVersion, secondaryRecord.expireTimerVersion),
|
||||
REGISTERED to RegisteredState.REGISTERED.id,
|
||||
SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName,
|
||||
SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName,
|
||||
|
@ -4591,6 +4634,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
// const val PNP = 7
|
||||
// const val PAYMENT_ACTIVATION = 8
|
||||
const val DELETE_SYNC = 9
|
||||
const val VERSIONED_EXPIRATION_TIMER = 10
|
||||
|
||||
// IMPORTANT: We cannot sore more than 32 capabilities in the bitmask.
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ object RecipientTableCursorUtil {
|
|||
messageRingtone = Util.uri(cursor.requireString(RecipientTable.MESSAGE_RINGTONE)),
|
||||
callRingtone = Util.uri(cursor.requireString(RecipientTable.CALL_RINGTONE)),
|
||||
expireMessages = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME),
|
||||
expireTimerVersion = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION),
|
||||
registered = RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED)),
|
||||
profileKey = profileKey,
|
||||
expiringProfileKeyCredential = expiringProfileKeyCredential,
|
||||
|
@ -175,7 +176,8 @@ object RecipientTableCursorUtil {
|
|||
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
|
||||
return RecipientRecord.Capabilities(
|
||||
rawBits = capabilities,
|
||||
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt())
|
||||
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt()),
|
||||
versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH).toInt())
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V237_ResetGroupForc
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V238_AddGroupSendEndorsementsColumns
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V239_MessageFullTextSearchEmojiSupport
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V240_MessageFullTextSearchSecureDelete
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V241_ExpireTimerVersion
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -198,10 +199,11 @@ object SignalDatabaseMigrations {
|
|||
237 to V237_ResetGroupForceUpdateTimestamps,
|
||||
238 to V238_AddGroupSendEndorsementsColumns,
|
||||
239 to V239_MessageFullTextSearchEmojiSupport,
|
||||
240 to V240_MessageFullTextSearchSecureDelete
|
||||
240 to V240_MessageFullTextSearchSecureDelete,
|
||||
241 to V241_ExpireTimerVersion
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 240
|
||||
const val DATABASE_VERSION = 241
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Migration for new field tracking expiration timer version.
|
||||
*/
|
||||
object V241_ExpireTimerVersion : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE message ADD COLUMN expire_timer_version INTEGER DEFAULT 1 NOT NULL;")
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN message_expiration_time_version INTEGER DEFAULT 1 NOT NULL;")
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE recipient
|
||||
SET message_expiration_time_version = 2
|
||||
WHERE message_expiration_time > 0
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ public class InMemoryMessageRecord extends MessageRecord {
|
|||
-1,
|
||||
0,
|
||||
System.currentTimeMillis(),
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
Collections.emptyList(),
|
||||
|
|
|
@ -99,6 +99,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStarted;
|
||||
private final int expireTimerVersion;
|
||||
private final boolean unidentified;
|
||||
private final List<ReactionRecord> reactions;
|
||||
private final long serverTimestamp;
|
||||
|
@ -119,6 +120,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
int subscriptionId,
|
||||
long expiresIn,
|
||||
long expireStarted,
|
||||
int expireTimerVersion,
|
||||
boolean hasReadReceipt,
|
||||
boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions,
|
||||
|
@ -140,6 +142,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStarted = expireStarted;
|
||||
this.expireTimerVersion = expireTimerVersion;
|
||||
this.unidentified = unidentified;
|
||||
this.reactions = reactions;
|
||||
this.serverTimestamp = dateServer;
|
||||
|
@ -754,6 +757,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return expireStarted;
|
||||
}
|
||||
|
||||
public int getExpireTimerVersion() {
|
||||
return expireTimerVersion;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
int subscriptionId,
|
||||
long expiresIn,
|
||||
long expireStarted,
|
||||
int expireTimerVersion,
|
||||
boolean viewOnce,
|
||||
boolean hasReadReceipt,
|
||||
@Nullable Quote quote,
|
||||
|
@ -121,7 +122,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
{
|
||||
super(id, body, fromRecipient, fromDeviceId, toRecipient,
|
||||
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt,
|
||||
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, hasReadReceipt,
|
||||
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, expireTimerVersion, hasReadReceipt,
|
||||
unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, messageExtras);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
|
@ -323,7 +324,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
|
||||
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
|
||||
|
@ -331,7 +332,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
|
||||
public @NonNull MmsMessageRecord withoutQuote() {
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
|
||||
|
@ -353,7 +354,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(slideAttachments);
|
||||
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), slideDeck,
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
|
||||
|
@ -361,7 +362,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
|
||||
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
|
||||
|
@ -370,7 +371,7 @@ public class MmsMessageRecord extends MessageRecord {
|
|||
|
||||
public @NonNull MmsMessageRecord withCall(@Nullable CallTable.Call call) {
|
||||
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), hasDeliveryReceipt(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), getExpireTimerVersion(), isViewOnce(),
|
||||
hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras());
|
||||
|
|
|
@ -43,6 +43,7 @@ data class RecipientRecord(
|
|||
val messageRingtone: Uri?,
|
||||
val callRingtone: Uri?,
|
||||
val expireMessages: Int,
|
||||
val expireTimerVersion: Int,
|
||||
val registered: RegisteredState,
|
||||
val profileKey: ByteArray?,
|
||||
val expiringProfileKeyCredential: ExpiringProfileKeyCredential?,
|
||||
|
@ -119,13 +120,15 @@ data class RecipientRecord(
|
|||
|
||||
data class Capabilities(
|
||||
val rawBits: Long,
|
||||
val deleteSync: Recipient.Capability
|
||||
val deleteSync: Recipient.Capability,
|
||||
val versionedExpirationTimer: Recipient.Capability
|
||||
) {
|
||||
companion object {
|
||||
@JvmField
|
||||
val UNKNOWN = Capabilities(
|
||||
0,
|
||||
Recipient.Capability.UNKNOWN
|
||||
rawBits = 0,
|
||||
deleteSync = Recipient.Capability.UNKNOWN,
|
||||
versionedExpirationTimer = Recipient.Capability.UNKNOWN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,6 +266,7 @@ public class IndividualSendJob extends PushSendJob {
|
|||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withExpireTimerVersion(message.getExpireTimerVersion())
|
||||
.withViewOnce(message.isViewOnce())
|
||||
.withProfileKey(profileKey.orElse(null))
|
||||
.withSticker(sticker.orElse(null))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
|
||||
|
@ -92,7 +93,14 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo
|
|||
}
|
||||
|
||||
if (contact.expirationTimer.isPresent) {
|
||||
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get())
|
||||
if (contact.expirationTimerVersion.isPresent && contact.expirationTimerVersion.get() > recipient.expireTimerVersion) {
|
||||
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get(), contact.expirationTimerVersion.orElse(1))
|
||||
} else if (contact.expirationTimerVersion.isAbsent()) {
|
||||
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
|
||||
recipients.setExpireMessagesWithoutIncrementingVersion(recipient.id, contact.expirationTimer.get())
|
||||
} else {
|
||||
Log.w(TAG, "[ContactSync] ${recipient.id} was synced with an old expiration timer. Ignoring. Recieved: ${contact.expirationTimerVersion.get()} Current: ${recipient.expireTimerVersion}")
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.profileKey.isPresent) {
|
||||
|
|
|
@ -170,6 +170,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
|
||||
recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds())
|
||||
: Optional.empty(),
|
||||
Optional.of(recipient.getExpireTimerVersion()),
|
||||
Optional.ofNullable(inboxPositions.get(recipientId)),
|
||||
archived.contains(recipientId)));
|
||||
|
||||
|
@ -219,13 +220,14 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Set<RecipientId> archived = SignalDatabase.threads().getArchivedRecipients();
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
Optional<IdentityRecord> identity = AppDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId());
|
||||
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
|
||||
Optional<String> name = Optional.ofNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
boolean blocked = recipient.isBlocked();
|
||||
Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.empty();
|
||||
Optional<Integer> inboxPosition = Optional.ofNullable(inboxPositions.get(recipient.getId()));
|
||||
Optional<IdentityRecord> identity = AppDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId());
|
||||
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
|
||||
Optional<String> name = Optional.ofNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
boolean blocked = recipient.isBlocked();
|
||||
Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.empty();
|
||||
Optional<Integer> expireTimerVersion = Optional.of(recipient.getExpireTimerVersion());
|
||||
Optional<Integer> inboxPosition = Optional.ofNullable(inboxPositions.get(recipient.getId()));
|
||||
|
||||
out.write(new DeviceContact(recipient.getAci(),
|
||||
recipient.getE164(),
|
||||
|
@ -235,6 +237,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
verified,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion,
|
||||
inboxPosition,
|
||||
archived.contains(recipient.getId())));
|
||||
}
|
||||
|
@ -252,6 +255,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional.empty(),
|
||||
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
|
||||
self.getExpiresInSeconds() > 0 ? Optional.of(self.getExpiresInSeconds()) : Optional.empty(),
|
||||
Optional.of(self.getExpireTimerVersion()),
|
||||
Optional.ofNullable(inboxPositions.get(self.getId())),
|
||||
archived.contains(self.getId())));
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
|
|||
profileKey,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
false));
|
||||
|
||||
out.close();
|
||||
|
|
|
@ -221,6 +221,11 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
|
||||
}
|
||||
|
||||
if (!Recipient.self().getVersionedExpirationTimerCapability().isSupported() && capabilities.isVersionedExpirationTimer()) {
|
||||
Log.d(TAG, "Transitioned to versioned expiration timer capable, notify linked devices in case we were the last one");
|
||||
AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
|
||||
}
|
||||
|
||||
SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities);
|
||||
}
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ object DataMessageProcessor {
|
|||
when {
|
||||
message.isInvalid -> handleInvalidMessage(context, senderRecipient.id, groupId, envelope.timestamp!!)
|
||||
message.isEndSession -> insertResult = handleEndSessionMessage(context, senderRecipient.id, envelope, metadata)
|
||||
message.isExpirationUpdate -> insertResult = handleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient.id, groupId, message.expireTimerDuration, receivedTime, false)
|
||||
message.isExpirationUpdate -> insertResult = handleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient.id, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime, false)
|
||||
message.isStoryReaction -> insertResult = handleStoryReaction(context, envelope, metadata, message, senderRecipient.id, groupId)
|
||||
message.reaction != null -> messageId = handleReaction(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry)
|
||||
message.hasRemoteDelete -> messageId = handleRemoteDelete(context, envelope, message, senderRecipient.id, earlyMessageCacheEntry)
|
||||
|
@ -321,10 +321,11 @@ object DataMessageProcessor {
|
|||
private fun handleExpirationUpdate(
|
||||
envelope: Envelope,
|
||||
metadata: EnvelopeMetadata,
|
||||
senderRecipientId: RecipientId,
|
||||
senderRecipient: Recipient,
|
||||
threadRecipientId: RecipientId,
|
||||
groupId: GroupId.V2?,
|
||||
expiresIn: Duration,
|
||||
expireTimerVersion: Int?,
|
||||
receivedTime: Long,
|
||||
sideEffect: Boolean
|
||||
): InsertResult? {
|
||||
|
@ -340,10 +341,15 @@ object DataMessageProcessor {
|
|||
return null
|
||||
}
|
||||
|
||||
if (expireTimerVersion != null && expireTimerVersion < senderRecipient.expireTimerVersion) {
|
||||
log(envelope.timestamp!!, "Old expireTimerVersion. Received: $expireTimerVersion, Current: ${senderRecipient.expireTimerVersion}. Ignoring.")
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val mediaMessage = IncomingMessage(
|
||||
type = MessageType.EXPIRATION_UPDATE,
|
||||
from = senderRecipientId,
|
||||
from = senderRecipient.id,
|
||||
sentTimeMillis = envelope.timestamp!! - if (sideEffect) 1 else 0,
|
||||
serverTimeMillis = envelope.serverTimestamp!!,
|
||||
receivedTimeMillis = receivedTime,
|
||||
|
@ -353,7 +359,13 @@ object DataMessageProcessor {
|
|||
)
|
||||
|
||||
val insertResult: InsertResult? = SignalDatabase.messages.insertMessageInbox(mediaMessage, -1).orNull()
|
||||
SignalDatabase.recipients.setExpireMessages(threadRecipientId, expiresIn.inWholeSeconds.toInt())
|
||||
|
||||
if (expireTimerVersion != null) {
|
||||
SignalDatabase.recipients.setExpireMessages(threadRecipientId, expiresIn.inWholeSeconds.toInt(), expireTimerVersion)
|
||||
} else {
|
||||
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
|
||||
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(threadRecipientId, expiresIn.inWholeSeconds.toInt())
|
||||
}
|
||||
|
||||
if (insertResult != null) {
|
||||
return insertResult
|
||||
|
@ -372,15 +384,16 @@ object DataMessageProcessor {
|
|||
fun handlePossibleExpirationUpdate(
|
||||
envelope: Envelope,
|
||||
metadata: EnvelopeMetadata,
|
||||
senderRecipientId: RecipientId,
|
||||
senderRecipient: Recipient,
|
||||
threadRecipient: Recipient,
|
||||
groupId: GroupId.V2?,
|
||||
expiresIn: Duration,
|
||||
expireTimerVersion: Int?,
|
||||
receivedTime: Long
|
||||
) {
|
||||
if (threadRecipient.expiresInSeconds.toLong() != expiresIn.inWholeSeconds) {
|
||||
if (threadRecipient.expiresInSeconds.toLong() != expiresIn.inWholeSeconds || ((expireTimerVersion ?: -1) > threadRecipient.expireTimerVersion)) {
|
||||
warn(envelope.timestamp!!, "Message expire time didn't match thread expire time. Handling timer update.")
|
||||
handleExpirationUpdate(envelope, metadata, senderRecipientId, threadRecipient.id, groupId, expiresIn, receivedTime, true)
|
||||
handleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient.id, groupId, expiresIn, expireTimerVersion, receivedTime, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -741,7 +754,7 @@ object DataMessageProcessor {
|
|||
threadRecipient = senderRecipient
|
||||
}
|
||||
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime)
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
|
||||
|
||||
if (message.hasGroupContext) {
|
||||
parentStoryId = GroupReply(storyMessageId.id)
|
||||
|
@ -892,7 +905,7 @@ object DataMessageProcessor {
|
|||
val attachments: List<Attachment> = message.attachments.toPointersWithinLimit()
|
||||
val messageRanges: BodyRangeList? = if (message.bodyRanges.isNotEmpty()) message.bodyRanges.asSequence().take(BODY_RANGE_PROCESSING_LIMIT).filter { it.mentionAci == null }.toList().toBodyRangeList() else null
|
||||
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime)
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
|
||||
|
||||
val mediaMessage = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
|
@ -972,7 +985,7 @@ object DataMessageProcessor {
|
|||
|
||||
val body = message.body ?: ""
|
||||
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient.id, threadRecipient, groupId, message.expireTimerDuration, receivedTime)
|
||||
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
|
||||
|
||||
notifyTypingStoppedFromIncomingMessage(context, senderRecipient, threadRecipient.id, metadata.sourceDeviceId)
|
||||
|
||||
|
|
|
@ -353,6 +353,7 @@ object SyncMessageProcessor {
|
|||
body = body,
|
||||
timestamp = sent.timestamp!!,
|
||||
expiresIn = targetMessage.expiresIn,
|
||||
expireTimerVersion = targetMessage.expireTimerVersion,
|
||||
isSecure = true,
|
||||
bodyRanges = bodyRanges,
|
||||
messageToEdit = targetMessage.id
|
||||
|
@ -366,6 +367,7 @@ object SyncMessageProcessor {
|
|||
sentTimeMillis = sent.timestamp!!,
|
||||
body = body,
|
||||
expiresIn = targetMessage.expiresIn,
|
||||
expireTimerVersion = targetMessage.expireTimerVersion,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = bodyRanges,
|
||||
|
@ -429,6 +431,7 @@ object SyncMessageProcessor {
|
|||
attachments = syncAttachments.ifEmpty { (targetMessage as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: emptyList() },
|
||||
timestamp = sent.timestamp!!,
|
||||
expiresIn = targetMessage.expiresIn,
|
||||
expireTimerVersion = targetMessage.expireTimerVersion,
|
||||
viewOnce = viewOnce,
|
||||
quote = quote,
|
||||
contacts = sharedContacts,
|
||||
|
@ -676,13 +679,26 @@ object SyncMessageProcessor {
|
|||
}
|
||||
|
||||
val recipient: Recipient = getSyncMessageDestination(sent)
|
||||
val expirationUpdateMessage: OutgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!, sent.message!!.expireTimerDuration.inWholeMilliseconds)
|
||||
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
|
||||
val expirationUpdateMessage: OutgoingMessage = OutgoingMessage.expirationUpdateMessage(
|
||||
threadRecipient = recipient,
|
||||
sentTimeMillis = if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!,
|
||||
expiresIn = sent.message!!.expireTimerDuration.inWholeMilliseconds,
|
||||
expireTimerVersion = sent.message!!.expireTimerVersion ?: 1
|
||||
)
|
||||
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt())
|
||||
if (sent.message?.expireTimerVersion == null) {
|
||||
// TODO [expireVersion] After unsupported builds expire, we can remove this branch
|
||||
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt())
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
} else if (sent.message!!.expireTimerVersion!! >= recipient.expireTimerVersion) {
|
||||
SignalDatabase.recipients.setExpireMessages(recipient.id, sent.message!!.expireTimerDuration.inWholeSeconds.toInt(), sent.message!!.expireTimerVersion!!)
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
} else {
|
||||
warn(sent.timestamp!!, "[SynchronizeExpiration] Ignoring expire timer update with old version. Received: ${sent.message!!.expireTimerVersion}, Current: ${recipient.expireTimerVersion}")
|
||||
}
|
||||
|
||||
return threadId
|
||||
}
|
||||
|
@ -749,7 +765,7 @@ object SyncMessageProcessor {
|
|||
isSecure = true
|
||||
)
|
||||
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) {
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
|
||||
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
|
||||
}
|
||||
|
||||
|
@ -814,7 +830,7 @@ object SyncMessageProcessor {
|
|||
isSecure = true
|
||||
)
|
||||
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) {
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
|
||||
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
|
||||
}
|
||||
|
||||
|
@ -861,7 +877,7 @@ object SyncMessageProcessor {
|
|||
val expiresInMillis = dataMessage.expireTimerDuration.inWholeMilliseconds
|
||||
val bodyRanges = dataMessage.bodyRanges.filter { it.mentionAci == null }.toBodyRangeList()
|
||||
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt()) {
|
||||
if (recipient.expiresInSeconds != dataMessage.expireTimerDuration.inWholeSeconds.toInt() || ((dataMessage.expireTimerVersion ?: -1) > recipient.expireTimerVersion)) {
|
||||
handleSynchronizeSentExpirationUpdate(sent, sideEffect = true)
|
||||
}
|
||||
|
||||
|
|
|
@ -151,9 +151,10 @@ public class ApplicationMigrations {
|
|||
static final int CONTACT_LINK_REBUILD = 106;
|
||||
static final int DELETE_SYNC_CAPABILITY = 107;
|
||||
static final int REBUILD_MESSAGE_FTS_INDEX_5 = 108;
|
||||
static final int EXPIRE_TIMER_CAPABILITY = 109;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 108;
|
||||
public static final int CURRENT_VERSION = 109;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
|
@ -688,6 +689,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.REBUILD_MESSAGE_FTS_INDEX_5, new RebuildMessageSearchIndexMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.EXPIRE_TIMER_CAPABILITY) {
|
||||
jobs.put(Version.EXPIRE_TIMER_CAPABILITY, new AttributesMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ data class OutgoingMessage(
|
|||
val body: String = "",
|
||||
val distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
val expiresIn: Long = 0L,
|
||||
val expireTimerVersion: Int = threadRecipient.expireTimerVersion,
|
||||
val isViewOnce: Boolean = false,
|
||||
val outgoingQuote: QuoteModel? = null,
|
||||
val storyType: StoryType = StoryType.NONE,
|
||||
|
@ -70,6 +71,7 @@ data class OutgoingMessage(
|
|||
attachments: List<Attachment> = emptyList(),
|
||||
timestamp: Long,
|
||||
expiresIn: Long = 0L,
|
||||
expireTimerVersion: Int = 1,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
|
@ -92,6 +94,7 @@ data class OutgoingMessage(
|
|||
attachments = attachments,
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
isViewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
storyType = storyType,
|
||||
|
@ -119,6 +122,7 @@ data class OutgoingMessage(
|
|||
body: String? = "",
|
||||
timestamp: Long,
|
||||
expiresIn: Long = 0L,
|
||||
expiresTimerVersion: Int = 1,
|
||||
viewOnce: Boolean = false,
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
linkPreviews: List<LinkPreview> = emptyList(),
|
||||
|
@ -132,6 +136,7 @@ data class OutgoingMessage(
|
|||
attachments = slideDeck.asAttachments(),
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn,
|
||||
expireTimerVersion = expiresTimerVersion,
|
||||
isViewOnce = viewOnce,
|
||||
storyType = storyType,
|
||||
linkPreviews = linkPreviews,
|
||||
|
@ -143,8 +148,8 @@ data class OutgoingMessage(
|
|||
|
||||
val subscriptionId = -1
|
||||
|
||||
fun withExpiry(expiresIn: Long): OutgoingMessage {
|
||||
return copy(expiresIn = expiresIn)
|
||||
fun withExpiry(expiresIn: Long, expireTimerVersion: Int): OutgoingMessage {
|
||||
return copy(expiresIn = expiresIn, expireTimerVersion = expireTimerVersion)
|
||||
}
|
||||
|
||||
fun stripAttachments(): OutgoingMessage {
|
||||
|
@ -351,12 +356,13 @@ data class OutgoingMessage(
|
|||
* Helper for creating expiration update messages.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun expirationUpdateMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage {
|
||||
fun expirationUpdateMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long, expireTimerVersion: Int): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
expiresIn = expiresIn,
|
||||
isExpirationUpdate = true,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
isUrgent = false,
|
||||
isSecure = true
|
||||
)
|
||||
|
|
|
@ -75,9 +75,10 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
long threadId;
|
||||
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
ParentStoryId parentStoryId = groupStoryId != Long.MIN_VALUE ? ParentStoryId.deserialize(groupStoryId) : null;
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
int expireTimerVersion = recipient.getExpireTimerVersion();
|
||||
ParentStoryId parentStoryId = groupStoryId != Long.MIN_VALUE ? ParentStoryId.deserialize(groupStoryId) : null;
|
||||
|
||||
switch (replyMethod) {
|
||||
case GroupMessage: {
|
||||
|
@ -86,6 +87,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
new LinkedList<>(),
|
||||
System.currentTimeMillis(),
|
||||
expiresIn,
|
||||
expireTimerVersion,
|
||||
false,
|
||||
0,
|
||||
StoryType.NONE,
|
||||
|
|
|
@ -84,6 +84,7 @@ class Recipient(
|
|||
private val messageRingtoneUri: Uri? = null,
|
||||
private val callRingtoneUri: Uri? = null,
|
||||
val expiresInSeconds: Int = 0,
|
||||
val expireTimerVersion: Int = 1,
|
||||
private val registeredValue: RegisteredState = RegisteredState.UNKNOWN,
|
||||
val profileKey: ByteArray? = null,
|
||||
val expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null,
|
||||
|
@ -314,9 +315,12 @@ class Recipient(
|
|||
/** The notification channel, if both set and supported by the system. Otherwise null. */
|
||||
val notificationChannel: String? = if (!NotificationChannels.supported()) null else notificationChannelValue
|
||||
|
||||
/** The user's payment capability. */
|
||||
/** The user's capability to handle synchronizing deletes across linked devices. */
|
||||
val deleteSyncCapability: Capability = capabilities.deleteSync
|
||||
|
||||
/** The user's capability to handle tracking an expire timer version. */
|
||||
val versionedExpirationTimerCapability: Capability = capabilities.versionedExpirationTimer
|
||||
|
||||
/** The state around whether we can send sealed sender to this user. */
|
||||
val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) {
|
||||
SealedSenderAccessMode.DISABLED
|
||||
|
|
|
@ -167,6 +167,7 @@ object RecipientCreator {
|
|||
callVibrate = record.callVibrateState,
|
||||
isBlocked = record.isBlocked,
|
||||
expiresInSeconds = record.expireMessages,
|
||||
expireTimerVersion = record.expireTimerVersion,
|
||||
participantIdsValue = participantIds ?: LinkedList(),
|
||||
isActiveGroup = groupRecord.map { it.isActive }.orElse(false),
|
||||
profileName = record.signalProfileName,
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.ExpirationTimerUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
|
@ -324,21 +325,23 @@ public class RecipientUtil {
|
|||
/**
|
||||
* Checks if a universal timer is set and if the thread should have it set on it. Attempts to abort quickly and perform
|
||||
* minimal database access.
|
||||
*
|
||||
* @return The new expire timer version if the timer was set, otherwise null.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static boolean setAndSendUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
public static @Nullable Integer setAndSendUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
int defaultTimer = SignalStore.settings().getUniversalExpireTimer();
|
||||
if (defaultTimer == 0 || recipient.isGroup() || recipient.isDistributionList() || recipient.getExpiresInSeconds() != 0 || !recipient.isRegistered()) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (threadId == -1 || SignalDatabase.messages().canSetUniversalTimer(threadId)) {
|
||||
SignalDatabase.recipients().setExpireMessages(recipient.getId(), defaultTimer);
|
||||
OutgoingMessage outgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, System.currentTimeMillis(), defaultTimer * 1000L);
|
||||
int expireTimerVersion = ExpirationTimerUtil.setExpirationTimer(recipient.getId(), defaultTimer);
|
||||
OutgoingMessage outgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, System.currentTimeMillis(), defaultTimer * 1000L, expireTimerVersion);
|
||||
MessageSender.send(context, outgoingMessage, SignalDatabase.threads().getOrCreateThreadIdFor(recipient), MessageSender.SendType.SIGNAL, null, null);
|
||||
return true;
|
||||
return expireTimerVersion;
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
@ -105,12 +105,13 @@ public final class MultiShareSender {
|
|||
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
|
||||
Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId());
|
||||
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
|
||||
MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
List<Contact> contacts = multiShareArgs.getSharedContacts();
|
||||
SlideDeck slideDeck = new SlideDeck(primarySlideDeck);
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
|
||||
MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE;
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
int expireTimerVersion = recipient.getExpireTimerVersion();
|
||||
List<Contact> contacts = multiShareArgs.getSharedContacts();
|
||||
SlideDeck slideDeck = new SlideDeck(primarySlideDeck);
|
||||
|
||||
boolean needsSplit = message != null &&
|
||||
message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
|
@ -138,6 +139,7 @@ public final class MultiShareSender {
|
|||
sendType,
|
||||
threadId,
|
||||
expiresIn,
|
||||
expireTimerVersion,
|
||||
multiShareArgs.isViewOnce(),
|
||||
mentions,
|
||||
recipientSearchKey.isStory(),
|
||||
|
@ -182,6 +184,7 @@ public final class MultiShareSender {
|
|||
@NonNull MessageSendType sendType,
|
||||
long threadId,
|
||||
long expiresIn,
|
||||
int expireTimerVersion,
|
||||
boolean isViewOnce,
|
||||
@NonNull List<Mention> validatedMentions,
|
||||
boolean isStory,
|
||||
|
@ -221,6 +224,7 @@ public final class MultiShareSender {
|
|||
body,
|
||||
sentTimestamps.getMillis(0),
|
||||
0L,
|
||||
1,
|
||||
false,
|
||||
storyType.toTextStoryType(),
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
|
||||
|
@ -260,6 +264,7 @@ public final class MultiShareSender {
|
|||
body,
|
||||
sentTimestamps.getMillis(i),
|
||||
0L,
|
||||
1,
|
||||
false,
|
||||
storyType,
|
||||
Collections.emptyList(),
|
||||
|
@ -277,6 +282,7 @@ public final class MultiShareSender {
|
|||
body,
|
||||
sentTimestamps.getMillis(0),
|
||||
expiresIn,
|
||||
expireTimerVersion,
|
||||
isViewOnce,
|
||||
StoryType.NONE,
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
|
||||
|
|
|
@ -513,8 +513,12 @@ public class MessageSender {
|
|||
}
|
||||
|
||||
private static @NonNull OutgoingMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMessage outgoingMessage, long threadId) {
|
||||
if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
|
||||
return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()));
|
||||
if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0) {
|
||||
Integer expireTimerVersion = RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId);
|
||||
|
||||
if (expireTimerVersion != null) {
|
||||
return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()), expireTimerVersion);
|
||||
}
|
||||
}
|
||||
return outgoingMessage;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* This exists as a temporary shim to improve the callsites where we'll be setting the expiration timer.
|
||||
*
|
||||
* Until the versions that don't understand expiration timers expire, we'll have to check capabilities before incrementing the version.
|
||||
*
|
||||
* After those old clients expire, we can remove this shim entirely and call the RecipientTable methods directly.
|
||||
*/
|
||||
object ExpirationTimerUtil {
|
||||
|
||||
@JvmStatic
|
||||
fun setExpirationTimer(recipientId: RecipientId, expirationTimeSeconds: Int): Int {
|
||||
val selfCapable = Recipient.self().versionedExpirationTimerCapability == Recipient.Capability.SUPPORTED
|
||||
val recipientCapable = Recipient.resolved(recipientId).let { it.versionedExpirationTimerCapability == Recipient.Capability.SUPPORTED || it.expireTimerVersion > 2 }
|
||||
|
||||
return if (selfCapable && recipientCapable) {
|
||||
SignalDatabase.recipients.setExpireMessagesAndIncrementVersion(recipientId, expirationTimeSeconds)
|
||||
} else {
|
||||
SignalDatabase.recipients.setExpireMessagesWithoutIncrementingVersion(recipientId, expirationTimeSeconds)
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ object RecipientDatabaseTestUtils {
|
|||
messageRingtone: Uri = Uri.EMPTY,
|
||||
callRingtone: Uri = Uri.EMPTY,
|
||||
expireMessages: Int = 0,
|
||||
expireTimerVersion: Int = 1,
|
||||
registered: RecipientTable.RegisteredState = RecipientTable.RegisteredState.REGISTERED,
|
||||
profileKey: ByteArray = Random.nextBytes(32),
|
||||
expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null,
|
||||
|
@ -107,6 +108,7 @@ object RecipientDatabaseTestUtils {
|
|||
messageRingtone = messageRingtone,
|
||||
callRingtone = callRingtone,
|
||||
expireMessages = expireMessages,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
registered = registered,
|
||||
profileKey = profileKey,
|
||||
expiringProfileKeyCredential = expiringProfileKeyCredential,
|
||||
|
@ -124,7 +126,8 @@ object RecipientDatabaseTestUtils {
|
|||
sealedSenderAccessMode = sealedSenderAccessMode,
|
||||
capabilities = RecipientRecord.Capabilities(
|
||||
rawBits = capabilities,
|
||||
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt())
|
||||
deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()),
|
||||
versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.VERSIONED_EXPIRATION_TIMER, RecipientTable.Capabilities.BIT_LENGTH).toInt())
|
||||
),
|
||||
storageId = storageId,
|
||||
mentionSetting = mentionSetting,
|
||||
|
|
|
@ -20,6 +20,7 @@ object TestMms {
|
|||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
receivedTimestampMillis: Long = System.currentTimeMillis(),
|
||||
expiresIn: Long = 0,
|
||||
expireTimerVersion: Int = 1,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
type: Long = MessageTypes.BASE_INBOX_TYPE,
|
||||
|
@ -29,23 +30,24 @@ object TestMms {
|
|||
storyType: StoryType = StoryType.NONE
|
||||
): Long {
|
||||
val message = OutgoingMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
null
|
||||
recipient = recipient,
|
||||
body = body,
|
||||
attachments = emptyList(),
|
||||
timestamp = sentTimeMillis,
|
||||
expiresIn = expiresIn,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
storyType = storyType,
|
||||
parentStoryId = null,
|
||||
isStoryReaction = false,
|
||||
quote = null,
|
||||
contacts = emptyList(),
|
||||
previews = emptyList(),
|
||||
mentions = emptyList(),
|
||||
networkFailures = emptySet(),
|
||||
mismatches = emptySet(),
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
return insert(
|
||||
|
@ -61,7 +63,7 @@ object TestMms {
|
|||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
private fun insert(
|
||||
db: SQLiteDatabase,
|
||||
message: OutgoingMessage,
|
||||
recipientId: RecipientId = message.threadRecipient.id,
|
||||
|
@ -81,6 +83,7 @@ object TestMms {
|
|||
put(MessageTable.DATE_RECEIVED, receivedTimestampMillis)
|
||||
put(MessageTable.SMS_SUBSCRIPTION_ID, message.subscriptionId)
|
||||
put(MessageTable.EXPIRES_IN, message.expiresIn)
|
||||
put(MessageTable.EXPIRE_TIMER_VERSION, message.expireTimerVersion)
|
||||
put(MessageTable.VIEW_ONCE, message.isViewOnce)
|
||||
put(MessageTable.FROM_RECIPIENT_ID, recipientId.serialize())
|
||||
put(MessageTable.TO_RECIPIENT_ID, recipientId.serialize())
|
||||
|
|
|
@ -141,6 +141,7 @@ object FakeMessageRecords {
|
|||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = -1,
|
||||
expireStarted: Long = -1,
|
||||
expireTimerVersion: Int = individualRecipient.expireTimerVersion,
|
||||
viewOnce: Boolean = false,
|
||||
hasReadReceipt: Boolean = false,
|
||||
quote: Quote? = null,
|
||||
|
@ -178,6 +179,7 @@ object FakeMessageRecords {
|
|||
subscriptionId,
|
||||
expiresIn,
|
||||
expireStarted,
|
||||
expireTimerVersion,
|
||||
viewOnce,
|
||||
hasReadReceipt,
|
||||
quote,
|
||||
|
|
|
@ -1059,6 +1059,7 @@ public class SignalServiceMessageSender {
|
|||
if (message.getExpiresInSeconds() > 0) {
|
||||
builder.expireTimer(message.getExpiresInSeconds());
|
||||
}
|
||||
builder.expireTimerVersion(message.getExpireTimerVersion());
|
||||
|
||||
if (message.getProfileKey().isPresent()) {
|
||||
builder.profileKey(ByteString.of(message.getProfileKey().get()));
|
||||
|
|
|
@ -55,6 +55,7 @@ class AccountAttributes @JsonCreator constructor(
|
|||
|
||||
data class Capabilities @JsonCreator constructor(
|
||||
@JsonProperty val storage: Boolean,
|
||||
@JsonProperty val deleteSync: Boolean
|
||||
@JsonProperty val deleteSync: Boolean,
|
||||
@JsonProperty val expireTimerVersion: Boolean
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ class SignalServiceDataMessage private constructor(
|
|||
val body: Optional<String>,
|
||||
val isEndSession: Boolean,
|
||||
val expiresInSeconds: Int,
|
||||
val expireTimerVersion: Int,
|
||||
val isExpirationUpdate: Boolean,
|
||||
val profileKey: Optional<ByteArray>,
|
||||
val isProfileKeyUpdate: Boolean,
|
||||
|
@ -79,6 +80,7 @@ class SignalServiceDataMessage private constructor(
|
|||
private var body: String? = null
|
||||
private var endSession: Boolean = false
|
||||
private var expiresInSeconds: Int = 0
|
||||
private var expireTimerVersion: Int = 1
|
||||
private var expirationUpdate: Boolean = false
|
||||
private var profileKey: ByteArray? = null
|
||||
private var profileKeyUpdate: Boolean = false
|
||||
|
@ -133,6 +135,11 @@ class SignalServiceDataMessage private constructor(
|
|||
return this
|
||||
}
|
||||
|
||||
fun withExpireTimerVersion(expireTimerVersion: Int): Builder {
|
||||
this.expireTimerVersion = expireTimerVersion
|
||||
return this
|
||||
}
|
||||
|
||||
fun withProfileKey(profileKey: ByteArray?): Builder {
|
||||
this.profileKey = profileKey
|
||||
return this
|
||||
|
@ -225,6 +232,7 @@ class SignalServiceDataMessage private constructor(
|
|||
body = body.emptyIfStringEmpty(),
|
||||
isEndSession = endSession,
|
||||
expiresInSeconds = expiresInSeconds,
|
||||
expireTimerVersion = expireTimerVersion,
|
||||
isExpirationUpdate = expirationUpdate,
|
||||
profileKey = profileKey.asOptional(),
|
||||
isProfileKeyUpdate = profileKeyUpdate,
|
||||
|
|
|
@ -21,6 +21,7 @@ public class DeviceContact {
|
|||
private final Optional<VerifiedMessage> verified;
|
||||
private final Optional<ProfileKey> profileKey;
|
||||
private final Optional<Integer> expirationTimer;
|
||||
private final Optional<Integer> expirationTimerVersion;
|
||||
private final Optional<Integer> inboxPosition;
|
||||
private final boolean archived;
|
||||
|
||||
|
@ -32,6 +33,7 @@ public class DeviceContact {
|
|||
Optional<VerifiedMessage> verified,
|
||||
Optional<ProfileKey> profileKey,
|
||||
Optional<Integer> expirationTimer,
|
||||
Optional<Integer> expirationTimerVersion,
|
||||
Optional<Integer> inboxPosition,
|
||||
boolean archived)
|
||||
{
|
||||
|
@ -39,16 +41,17 @@ public class DeviceContact {
|
|||
throw new IllegalArgumentException("Must have either ACI or E164");
|
||||
}
|
||||
|
||||
this.aci = aci;
|
||||
this.e164 = e164;
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.color = color;
|
||||
this.verified = verified;
|
||||
this.profileKey = profileKey;
|
||||
this.expirationTimer = expirationTimer;
|
||||
this.inboxPosition = inboxPosition;
|
||||
this.archived = archived;
|
||||
this.aci = aci;
|
||||
this.e164 = e164;
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.color = color;
|
||||
this.verified = verified;
|
||||
this.profileKey = profileKey;
|
||||
this.expirationTimer = expirationTimer;
|
||||
this.expirationTimerVersion = expirationTimerVersion;
|
||||
this.inboxPosition = inboxPosition;
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
public Optional<DeviceContactAvatar> getAvatar() {
|
||||
|
@ -83,6 +86,10 @@ public class DeviceContact {
|
|||
return expirationTimer;
|
||||
}
|
||||
|
||||
public Optional<Integer> getExpirationTimerVersion() {
|
||||
return expirationTimerVersion;
|
||||
}
|
||||
|
||||
public Optional<Integer> getInboxPosition() {
|
||||
return inboxPosition;
|
||||
}
|
||||
|
|
|
@ -47,16 +47,17 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
|||
throw new IOException("Missing contact address!");
|
||||
}
|
||||
|
||||
Optional<ACI> aci = Optional.ofNullable(ACI.parseOrNull(details.aci));
|
||||
Optional<String> e164 = Optional.ofNullable(details.number);
|
||||
Optional<String> name = Optional.ofNullable(details.name);
|
||||
Optional<DeviceContactAvatar> avatar = Optional.empty();
|
||||
Optional<String> color = details.color != null ? Optional.of(details.color) : Optional.empty();
|
||||
Optional<VerifiedMessage> verified = Optional.empty();
|
||||
Optional<ProfileKey> profileKey = Optional.empty();
|
||||
Optional<Integer> expireTimer = Optional.empty();
|
||||
Optional<Integer> inboxPosition = Optional.empty();
|
||||
boolean archived = false;
|
||||
Optional<ACI> aci = Optional.ofNullable(ACI.parseOrNull(details.aci));
|
||||
Optional<String> e164 = Optional.ofNullable(details.number);
|
||||
Optional<String> name = Optional.ofNullable(details.name);
|
||||
Optional<DeviceContactAvatar> avatar = Optional.empty();
|
||||
Optional<String> color = details.color != null ? Optional.of(details.color) : Optional.empty();
|
||||
Optional<VerifiedMessage> verified = Optional.empty();
|
||||
Optional<ProfileKey> profileKey = Optional.empty();
|
||||
Optional<Integer> expireTimer = Optional.empty();
|
||||
Optional<Integer> expireTimerVersion = Optional.empty();
|
||||
Optional<Integer> inboxPosition = Optional.empty();
|
||||
boolean archived = false;
|
||||
|
||||
if (details.avatar != null && details.avatar.length != null) {
|
||||
long avatarLength = details.avatar.length;
|
||||
|
@ -103,13 +104,17 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
|||
expireTimer = Optional.of(details.expireTimer);
|
||||
}
|
||||
|
||||
if (details.expireTimerVersion != null && details.expireTimerVersion > 0) {
|
||||
expireTimerVersion = Optional.of(details.expireTimerVersion);
|
||||
}
|
||||
|
||||
if (details.inboxPosition != null) {
|
||||
inboxPosition = Optional.of(details.inboxPosition);
|
||||
}
|
||||
|
||||
archived = details.archived;
|
||||
|
||||
return new DeviceContact(aci, e164, name, avatar, color, verified, profileKey, expireTimer, inboxPosition, archived);
|
||||
return new DeviceContact(aci, e164, name, avatar, color, verified, profileKey, expireTimer, expireTimerVersion, inboxPosition, archived);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -195,12 +195,16 @@ public class SignalServiceProfile {
|
|||
@JsonProperty
|
||||
private boolean deleteSync;
|
||||
|
||||
@JsonProperty
|
||||
private boolean versionedExpirationTimer;
|
||||
|
||||
@JsonCreator
|
||||
public Capabilities() {}
|
||||
|
||||
public Capabilities(boolean storage, boolean deleteSync) {
|
||||
this.storage = storage;
|
||||
this.deleteSync = deleteSync;
|
||||
public Capabilities(boolean storage, boolean deleteSync, boolean versionedExpirationTimer) {
|
||||
this.storage = storage;
|
||||
this.deleteSync = deleteSync;
|
||||
this.versionedExpirationTimer = versionedExpirationTimer;
|
||||
}
|
||||
|
||||
public boolean isStorage() {
|
||||
|
@ -210,6 +214,10 @@ public class SignalServiceProfile {
|
|||
public boolean isDeleteSync() {
|
||||
return deleteSync;
|
||||
}
|
||||
|
||||
public boolean isVersionedExpirationTimer() {
|
||||
return versionedExpirationTimer;
|
||||
}
|
||||
}
|
||||
|
||||
public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() {
|
||||
|
|
|
@ -332,6 +332,7 @@ message DataMessage {
|
|||
optional GroupContextV2 groupV2 = 15;
|
||||
optional uint32 flags = 4;
|
||||
optional uint32 expireTimer = 5;
|
||||
optional uint32 expireTimerVersion = 23;
|
||||
optional bytes profileKey = 6;
|
||||
optional uint64 timestamp = 7;
|
||||
optional Quote quote = 8;
|
||||
|
@ -793,17 +794,18 @@ message ContactDetails {
|
|||
optional uint32 length = 2;
|
||||
}
|
||||
|
||||
optional string number = 1;
|
||||
optional string aci = 9;
|
||||
optional string name = 2;
|
||||
optional Avatar avatar = 3;
|
||||
optional string color = 4;
|
||||
optional Verified verified = 5;
|
||||
optional bytes profileKey = 6;
|
||||
reserved /*blocked*/ 7;
|
||||
optional uint32 expireTimer = 8;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
optional string number = 1;
|
||||
optional string aci = 9;
|
||||
optional string name = 2;
|
||||
optional Avatar avatar = 3;
|
||||
optional string color = 4;
|
||||
optional Verified verified = 5;
|
||||
optional bytes profileKey = 6;
|
||||
reserved /*blocked*/ 7;
|
||||
optional uint32 expireTimer = 8;
|
||||
optional uint32 expireTimerVersion = 12;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
}
|
||||
|
||||
message GroupDetails {
|
||||
|
@ -817,17 +819,17 @@ message GroupDetails {
|
|||
optional string e164 = 2;
|
||||
}
|
||||
|
||||
optional bytes id = 1;
|
||||
optional string name = 2;
|
||||
repeated string membersE164 = 3;
|
||||
repeated Member members = 9;
|
||||
optional Avatar avatar = 4;
|
||||
optional bool active = 5 [default = true];
|
||||
optional uint32 expireTimer = 6;
|
||||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
optional bytes id = 1;
|
||||
optional string name = 2;
|
||||
repeated string membersE164 = 3;
|
||||
repeated Member members = 9;
|
||||
optional Avatar avatar = 4;
|
||||
optional bool active = 5 [default = true];
|
||||
optional uint32 expireTimer = 6;
|
||||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
}
|
||||
|
||||
message PaymentAddress {
|
||||
|
|
|
@ -40,6 +40,7 @@ public class DeviceContactsInputStreamTest {
|
|||
Optional.of(generateProfileKey()),
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
false
|
||||
);
|
||||
|
||||
|
@ -53,6 +54,7 @@ public class DeviceContactsInputStreamTest {
|
|||
Optional.of(generateProfileKey()),
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
false
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue