Setup backupV2 infrastructure and testing.

Co-authored-by: Clark Chen <clark@signal.org>
This commit is contained in:
Greyson Parrelli
2023-09-13 15:15:33 -04:00
committed by Cody Henthorne
parent feb74d90f6
commit b540b5813e
47 changed files with 3782 additions and 274 deletions

View File

@@ -79,11 +79,11 @@ public abstract class DatabaseTable {
this.databaseHelper = databaseHelper;
}
protected SQLiteDatabase getReadableDatabase() {
public SQLiteDatabase getReadableDatabase() {
return databaseHelper.getSignalReadableDatabase();
}
protected SQLiteDatabase getWritableDatabase() {
public SQLiteDatabase getWritableDatabase() {
return databaseHelper.getSignalWritableDatabase();
}
}

View File

@@ -106,7 +106,7 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
val LIST_UI_PROJECTION = arrayOf(ID, NAME, RECIPIENT_ID, ALLOWS_REPLIES, IS_UNKNOWN, PRIVACY_MODE, SEARCH_NAME)
}
private object MembershipTable {
object MembershipTable {
const val TABLE_NAME = "distribution_list_member"
const val ID = "_id"

View File

@@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.forEach
import org.signal.core.util.readToList
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
@@ -21,9 +23,9 @@ class GroupReceiptTable(context: Context?, databaseHelper: SignalDatabase?) : Da
private const val ID = "_id"
const val MMS_ID = "mms_id"
const val RECIPIENT_ID = "address"
private const val STATUS = "status"
private const val TIMESTAMP = "timestamp"
private const val UNIDENTIFIED = "unidentified"
const val STATUS = "status"
const val TIMESTAMP = "timestamp"
const val UNIDENTIFIED = "unidentified"
const val STATUS_UNKNOWN = -1
const val STATUS_UNDELIVERED = 0
const val STATUS_DELIVERED = 1
@@ -127,14 +129,32 @@ class GroupReceiptTable(context: Context?, databaseHelper: SignalDatabase?) : Da
.from(TABLE_NAME)
.where("$MMS_ID = ?", mmsId)
.run()
.readToList { cursor ->
GroupReceiptInfo(
recipientId = RecipientId.from(cursor.requireLong(RECIPIENT_ID)),
status = cursor.requireInt(STATUS),
timestamp = cursor.requireLong(TIMESTAMP),
isUnidentified = cursor.requireBoolean(UNIDENTIFIED)
)
}
.readToList { it.toGroupReceiptInfo() }
}
fun getGroupReceiptInfoForMessages(ids: Set<Long>): Map<Long, List<GroupReceiptInfo>> {
if (ids.isEmpty()) {
return emptyMap()
}
val messageIdsToGroupReceipts: MutableMap<Long, MutableList<GroupReceiptInfo>> = mutableMapOf()
val args: List<Array<String>> = ids.map { SqlUtil.buildArgs(it) }
SqlUtil.buildCustomCollectionQuery("$MMS_ID = ?", args).forEach { query ->
readableDatabase
.select()
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.forEach { cursor ->
val messageId = cursor.requireLong(MMS_ID)
val receipts = messageIdsToGroupReceipts.getOrPut(messageId) { mutableListOf() }
receipts += cursor.toGroupReceiptInfo()
}
}
return messageIdsToGroupReceipts
}
fun deleteRowsForMessage(mmsId: Long) {
@@ -163,6 +183,15 @@ class GroupReceiptTable(context: Context?, databaseHelper: SignalDatabase?) : Da
.run()
}
private fun Cursor.toGroupReceiptInfo(): GroupReceiptInfo {
return GroupReceiptInfo(
recipientId = RecipientId.from(this.requireLong(RECIPIENT_ID)),
status = this.requireInt(STATUS),
timestamp = this.requireLong(TIMESTAMP),
isUnidentified = this.requireBoolean(UNIDENTIFIED)
)
}
data class GroupReceiptInfo(
val recipientId: RecipientId,
val status: Int,

View File

@@ -1770,7 +1770,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return threads.getOrCreateThreadIdFor(recipient)
}
private fun rawQueryWithAttachments(where: String, arguments: Array<String>?, reverse: Boolean = false, limit: Long = 0): Cursor {
fun rawQueryWithAttachments(where: String, arguments: Array<String>?, reverse: Boolean = false, limit: Long = 0): Cursor {
return rawQueryWithAttachments(MMS_PROJECTION_WITH_ATTACHMENTS, where, arguments, reverse, limit)
}

View File

@@ -22,10 +22,10 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database
private const val ID = "_id"
const val MESSAGE_ID = "message_id"
private const val AUTHOR_ID = "author_id"
private const val EMOJI = "emoji"
private const val DATE_SENT = "date_sent"
private const val DATE_RECEIVED = "date_received"
const val AUTHOR_ID = "author_id"
const val EMOJI = "emoji"
const val DATE_SENT = "date_sent"
const val DATE_RECEIVED = "date_received"
@JvmField
val CREATE_TABLE = """

View File

@@ -17,10 +17,7 @@ import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.exists
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalBlob
import org.signal.core.util.optionalBoolean
import org.signal.core.util.optionalInt
import org.signal.core.util.optionalLong
import org.signal.core.util.nullIfBlank
import org.signal.core.util.optionalString
import org.signal.core.util.or
import org.signal.core.util.orNull
@@ -29,7 +26,6 @@ import org.signal.core.util.readToSet
import org.signal.core.util.readToSingleBoolean
import org.signal.core.util.readToSingleLong
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
@@ -39,11 +35,9 @@ import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.InvalidKeyException
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.color.MaterialColor
@@ -58,6 +52,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.GroupTable.LegacyGroupInsertException
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil.getRecipientExtras
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
@@ -84,7 +79,6 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -93,12 +87,10 @@ import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.storage.StorageSyncModels
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.IdentityUtil
import org.thoughtcrime.securesms.util.ProfileUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ServiceId
@@ -113,7 +105,6 @@ import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
import java.io.Closeable
import java.io.IOException
import java.util.Arrays
import java.util.Collections
import java.util.LinkedList
import java.util.Objects
@@ -123,9 +114,9 @@ import kotlin.math.max
open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
companion object {
private val TAG = Log.tag(RecipientTable::class.java)
val TAG = Log.tag(RecipientTable::class.java)
companion object {
private val UNREGISTERED_LIFESPAN: Long = TimeUnit.DAYS.toMillis(30)
const val TABLE_NAME = "recipient"
@@ -651,6 +642,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
fun getAll(): RecipientIterator {
val cursor = readableDatabase
.select()
.from(TABLE_NAME)
.run()
return RecipientIterator(context, cursor)
}
/**
* Only call once to create initial release channel recipient.
*/
@@ -704,7 +704,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
readableDatabase.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null).use { cursor ->
return if (cursor != null && cursor.moveToNext()) {
getRecord(context, cursor)
RecipientTableCursorUtil.getRecord(context, cursor)
} else {
findRemappedIdRecord(id)
}
@@ -1113,7 +1113,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
readableDatabase.query(table, columns, query, args, "$TABLE_NAME.$ID", null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
out.add(getRecord(context, cursor))
out.add(RecipientTableCursorUtil.getRecord(context, cursor))
}
}
@@ -1709,9 +1709,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
fun setProfileName(id: RecipientId, profileName: ProfileName) {
val contentValues = ContentValues(1).apply {
put(PROFILE_GIVEN_NAME, profileName.givenName)
put(PROFILE_FAMILY_NAME, profileName.familyName)
put(PROFILE_JOINED_NAME, profileName.toString())
put(PROFILE_GIVEN_NAME, profileName.givenName.nullIfBlank())
put(PROFILE_FAMILY_NAME, profileName.familyName.nullIfBlank())
put(PROFILE_JOINED_NAME, profileName.toString().nullIfBlank())
}
if (update(id, contentValues)) {
rotateStorageId(id)
@@ -3177,7 +3177,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.where("$ID LIKE ? OR $ACI_COLUMN LIKE ? OR $PNI_COLUMN LIKE ?", "%$query%", "%$query%", "%$query%")
.run()
.readToList { cursor ->
getRecord(context, cursor)
RecipientTableCursorUtil.getRecord(context, cursor)
}
}
@@ -3650,7 +3650,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.run()
.use { cursor ->
return if (cursor.moveToFirst()) {
readCapabilities(cursor)
RecipientTableCursorUtil.readCapabilities(cursor)
} else {
null
}
@@ -4110,196 +4110,6 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
RecipientId.clearCache()
}
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
return getRecord(context, cursor, ID)
}
fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord {
val profileKeyString = cursor.requireString(PROFILE_KEY)
val expiringProfileKeyCredentialString = cursor.requireString(EXPIRING_PROFILE_KEY_CREDENTIAL)
var profileKey: ByteArray? = null
var expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null
if (profileKeyString != null) {
try {
profileKey = Base64.decode(profileKeyString)
} catch (e: IOException) {
Log.w(TAG, e)
}
if (expiringProfileKeyCredentialString != null) {
try {
val columnDataBytes = Base64.decode(expiringProfileKeyCredentialString)
val columnData = ExpiringProfileKeyCredentialColumnData.ADAPTER.decode(columnDataBytes)
if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) {
expiringProfileKeyCredential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray())
} else {
Log.i(TAG, "Out of date profile key credential data ignored on read")
}
} catch (e: InvalidInputException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
} catch (e: IOException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
}
}
}
val serializedWallpaper = cursor.requireBlob(WALLPAPER)
val chatWallpaper: ChatWallpaper? = if (serializedWallpaper != null) {
try {
ChatWallpaperFactory.create(Wallpaper.ADAPTER.decode(serializedWallpaper))
} catch (e: IOException) {
Log.w(TAG, "Failed to parse wallpaper.", e)
null
}
} else {
null
}
val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID)
val serializedChatColors = cursor.requireBlob(CHAT_COLORS)
val chatColors: ChatColors? = if (serializedChatColors != null) {
try {
forChatColor(forLongValue(customChatColorsId), ChatColor.ADAPTER.decode(serializedChatColors))
} catch (e: IOException) {
Log.w(TAG, "Failed to parse chat colors.", e)
null
}
} else {
null
}
val recipientId = RecipientId.from(cursor.requireLong(idColumnName))
val distributionListId: DistributionListId? = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID))
val avatarColor: AvatarColor = if (distributionListId != null) AvatarColor.UNKNOWN else AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR))
return RecipientRecord(
id = recipientId,
aci = ACI.parseOrNull(cursor.requireString(ACI_COLUMN)),
pni = PNI.parsePrefixedOrNull(cursor.requireString(PNI_COLUMN)),
username = cursor.requireString(USERNAME),
e164 = cursor.requireString(E164),
email = cursor.requireString(EMAIL),
groupId = GroupId.parseNullableOrThrow(cursor.requireString(GROUP_ID)),
distributionListId = distributionListId,
recipientType = RecipientType.fromId(cursor.requireInt(TYPE)),
isBlocked = cursor.requireBoolean(BLOCKED),
muteUntil = cursor.requireLong(MUTE_UNTIL),
messageVibrateState = VibrateState.fromId(cursor.requireInt(MESSAGE_VIBRATE)),
callVibrateState = VibrateState.fromId(cursor.requireInt(CALL_VIBRATE)),
messageRingtone = Util.uri(cursor.requireString(MESSAGE_RINGTONE)),
callRingtone = Util.uri(cursor.requireString(CALL_RINGTONE)),
expireMessages = cursor.requireInt(MESSAGE_EXPIRATION_TIME),
registered = RegisteredState.fromId(cursor.requireInt(REGISTERED)),
profileKey = profileKey,
expiringProfileKeyCredential = expiringProfileKeyCredential,
systemProfileName = ProfileName.fromParts(cursor.requireString(SYSTEM_GIVEN_NAME), cursor.requireString(SYSTEM_FAMILY_NAME)),
systemDisplayName = cursor.requireString(SYSTEM_JOINED_NAME),
systemContactPhotoUri = cursor.requireString(SYSTEM_PHOTO_URI),
systemPhoneLabel = cursor.requireString(SYSTEM_PHONE_LABEL),
systemContactUri = cursor.requireString(SYSTEM_CONTACT_URI),
signalProfileName = ProfileName.fromParts(cursor.requireString(PROFILE_GIVEN_NAME), cursor.requireString(PROFILE_FAMILY_NAME)),
signalProfileAvatar = cursor.requireString(PROFILE_AVATAR),
profileAvatarFileDetails = AvatarHelper.getAvatarFileDetails(context, recipientId),
profileSharing = cursor.requireBoolean(PROFILE_SHARING),
lastProfileFetch = cursor.requireLong(LAST_PROFILE_FETCH),
notificationChannel = cursor.requireString(NOTIFICATION_CHANNEL),
unidentifiedAccessMode = UnidentifiedAccessMode.fromMode(cursor.requireInt(SEALED_SENDER_MODE)),
capabilities = readCapabilities(cursor),
storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)),
mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)),
wallpaper = chatWallpaper,
chatColors = chatColors,
avatarColor = avatarColor,
about = cursor.requireString(ABOUT),
aboutEmoji = cursor.requireString(ABOUT_EMOJI),
syncExtras = getSyncExtras(cursor),
extras = getExtras(cursor),
hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON),
badges = parseBadgeList(cursor.requireBlob(BADGES)),
needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE),
hiddenState = Recipient.HiddenState.deserialize(cursor.requireInt(HIDDEN)),
callLinkRoomId = cursor.requireString(CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) }
)
}
private fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities {
val capabilities = cursor.requireLong(CAPABILITIES)
return RecipientRecord.Capabilities(
rawBits = capabilities,
groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()),
senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()),
announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()),
changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()),
storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()),
giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()),
pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()),
paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH).toInt())
)
}
private fun parseBadgeList(serializedBadgeList: ByteArray?): List<Badge> {
var badgeList: BadgeList? = null
if (serializedBadgeList != null) {
try {
badgeList = BadgeList.ADAPTER.decode(serializedBadgeList)
} catch (e: IOException) {
Log.w(TAG, e)
}
}
val badges: List<Badge>
if (badgeList != null) {
val protoBadges = badgeList.badges
badges = ArrayList(protoBadges.size)
for (protoBadge in protoBadges) {
badges.add(Badges.fromDatabaseBadge(protoBadge))
}
} else {
badges = emptyList()
}
return badges
}
private fun getSyncExtras(cursor: Cursor): RecipientRecord.SyncExtras {
val storageProtoRaw = cursor.optionalString(STORAGE_SERVICE_PROTO).orElse(null)
val storageProto = if (storageProtoRaw != null) Base64.decodeOrThrow(storageProtoRaw) else null
val archived = cursor.optionalBoolean(ThreadTable.ARCHIVED).orElse(false)
val forcedUnread = cursor.optionalInt(ThreadTable.READ).map { status: Int -> status == ThreadTable.ReadStatus.FORCED_UNREAD.serialize() }.orElse(false)
val groupMasterKey = cursor.optionalBlob(GroupTable.V2_MASTER_KEY).map { GroupUtil.requireMasterKey(it) }.orElse(null)
val identityKey = cursor.optionalString(IDENTITY_KEY).map { Base64.decodeOrThrow(it) }.orElse(null)
val identityStatus = cursor.optionalInt(IDENTITY_STATUS).map { VerifiedStatus.forState(it) }.orElse(VerifiedStatus.DEFAULT)
val unregisteredTimestamp = cursor.optionalLong(UNREGISTERED_TIMESTAMP).orElse(0)
val systemNickname = cursor.optionalString(SYSTEM_NICKNAME).orElse(null)
return RecipientRecord.SyncExtras(
storageProto = storageProto,
groupMasterKey = groupMasterKey,
identityKey = identityKey,
identityStatus = identityStatus,
isArchived = archived,
isForcedUnread = forcedUnread,
unregisteredTimestamp = unregisteredTimestamp,
systemNickname = systemNickname
)
}
private fun getExtras(cursor: Cursor): Recipient.Extras? {
return Recipient.Extras.from(getRecipientExtras(cursor))
}
private fun getRecipientExtras(cursor: Cursor): RecipientExtras? {
return cursor.optionalBlob(EXTRAS).map { b: ByteArray ->
try {
RecipientExtras.ADAPTER.decode(b)
} catch (e: IOException) {
Log.w(TAG, e)
throw AssertionError(e)
}
}.orElse(null)
}
private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) {
values.apply {
put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeWithPadding(record.profileKey) else null)
@@ -4430,6 +4240,28 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
class RecipientIterator(
private val context: Context,
private val cursor: Cursor
) : Iterator<RecipientRecord>, Closeable {
override fun hasNext(): Boolean {
return cursor.count != 0 && !cursor.isLast
}
override fun next(): RecipientRecord {
if (!cursor.moveToNext()) {
throw NoSuchElementException()
}
return RecipientTableCursorUtil.getRecord(context, cursor)
}
override fun close() {
cursor.close()
}
}
class MissingRecipientException(id: RecipientId?) : IllegalStateException("Failed to find recipient with ID: $id")
private class GetOrInsertResult(val recipientId: RecipientId, val neededInsert: Boolean)

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.util.Base64
import org.signal.core.util.Bitmask
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalBlob
import org.signal.core.util.optionalBoolean
import org.signal.core.util.optionalInt
import org.signal.core.util.optionalLong
import org.signal.core.util.optionalString
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.RecipientTable.Capabilities
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.IOException
import java.util.Arrays
object RecipientTableCursorUtil {
private val TAG = Log.tag(RecipientTableCursorUtil::class.java)
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
return getRecord(context, cursor, RecipientTable.ID)
}
fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord {
val profileKeyString = cursor.requireString(RecipientTable.PROFILE_KEY)
val expiringProfileKeyCredentialString = cursor.requireString(RecipientTable.EXPIRING_PROFILE_KEY_CREDENTIAL)
var profileKey: ByteArray? = null
var expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null
if (profileKeyString != null) {
try {
profileKey = Base64.decode(profileKeyString)
} catch (e: IOException) {
Log.w(TAG, e)
}
if (expiringProfileKeyCredentialString != null) {
try {
val columnDataBytes = Base64.decode(expiringProfileKeyCredentialString)
val columnData = ExpiringProfileKeyCredentialColumnData.ADAPTER.decode(columnDataBytes)
if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) {
expiringProfileKeyCredential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray())
} else {
Log.i(TAG, "Out of date profile key credential data ignored on read")
}
} catch (e: InvalidInputException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
} catch (e: IOException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
}
}
}
val serializedWallpaper = cursor.requireBlob(RecipientTable.WALLPAPER)
val chatWallpaper: ChatWallpaper? = if (serializedWallpaper != null) {
try {
ChatWallpaperFactory.create(Wallpaper.ADAPTER.decode(serializedWallpaper))
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Failed to parse wallpaper.", e)
null
}
} else {
null
}
val customChatColorsId = cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID)
val serializedChatColors = cursor.requireBlob(RecipientTable.CHAT_COLORS)
val chatColors: ChatColors? = if (serializedChatColors != null) {
try {
ChatColors.forChatColor(ChatColors.Id.forLongValue(customChatColorsId), ChatColor.ADAPTER.decode(serializedChatColors))
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Failed to parse chat colors.", e)
null
}
} else {
null
}
val recipientId = RecipientId.from(cursor.requireLong(idColumnName))
val distributionListId: DistributionListId? = DistributionListId.fromNullable(cursor.requireLong(RecipientTable.DISTRIBUTION_LIST_ID))
val avatarColor: AvatarColor = if (distributionListId != null) AvatarColor.UNKNOWN else AvatarColor.deserialize(cursor.requireString(RecipientTable.AVATAR_COLOR))
return RecipientRecord(
id = recipientId,
aci = ServiceId.ACI.parseOrNull(cursor.requireString(RecipientTable.ACI_COLUMN)),
pni = ServiceId.PNI.parsePrefixedOrNull(cursor.requireString(RecipientTable.PNI_COLUMN)),
username = cursor.requireString(RecipientTable.USERNAME),
e164 = cursor.requireString(RecipientTable.E164),
email = cursor.requireString(RecipientTable.EMAIL),
groupId = GroupId.parseNullableOrThrow(cursor.requireString(RecipientTable.GROUP_ID)),
distributionListId = distributionListId,
recipientType = RecipientTable.RecipientType.fromId(cursor.requireInt(RecipientTable.TYPE)),
isBlocked = cursor.requireBoolean(RecipientTable.BLOCKED),
muteUntil = cursor.requireLong(RecipientTable.MUTE_UNTIL),
messageVibrateState = RecipientTable.VibrateState.fromId(cursor.requireInt(RecipientTable.MESSAGE_VIBRATE)),
callVibrateState = RecipientTable.VibrateState.fromId(cursor.requireInt(RecipientTable.CALL_VIBRATE)),
messageRingtone = Util.uri(cursor.requireString(RecipientTable.MESSAGE_RINGTONE)),
callRingtone = Util.uri(cursor.requireString(RecipientTable.CALL_RINGTONE)),
expireMessages = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME),
registered = RegisteredState.fromId(cursor.requireInt(RecipientTable.REGISTERED)),
profileKey = profileKey,
expiringProfileKeyCredential = expiringProfileKeyCredential,
systemProfileName = ProfileName.fromParts(cursor.requireString(RecipientTable.SYSTEM_GIVEN_NAME), cursor.requireString(RecipientTable.SYSTEM_FAMILY_NAME)),
systemDisplayName = cursor.requireString(RecipientTable.SYSTEM_JOINED_NAME),
systemContactPhotoUri = cursor.requireString(RecipientTable.SYSTEM_PHOTO_URI),
systemPhoneLabel = cursor.requireString(RecipientTable.SYSTEM_PHONE_LABEL),
systemContactUri = cursor.requireString(RecipientTable.SYSTEM_CONTACT_URI),
signalProfileName = ProfileName.fromParts(cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME), cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME)),
signalProfileAvatar = cursor.requireString(RecipientTable.PROFILE_AVATAR),
profileAvatarFileDetails = AvatarHelper.getAvatarFileDetails(context, recipientId),
profileSharing = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
lastProfileFetch = cursor.requireLong(RecipientTable.LAST_PROFILE_FETCH),
notificationChannel = cursor.requireString(RecipientTable.NOTIFICATION_CHANNEL),
unidentifiedAccessMode = RecipientTable.UnidentifiedAccessMode.fromMode(cursor.requireInt(RecipientTable.SEALED_SENDER_MODE)),
capabilities = readCapabilities(cursor),
storageId = Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID)),
mentionSetting = RecipientTable.MentionSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)),
wallpaper = chatWallpaper,
chatColors = chatColors,
avatarColor = avatarColor,
about = cursor.requireString(RecipientTable.ABOUT),
aboutEmoji = cursor.requireString(RecipientTable.ABOUT_EMOJI),
syncExtras = getSyncExtras(cursor),
extras = getExtras(cursor),
hasGroupsInCommon = cursor.requireBoolean(RecipientTable.GROUPS_IN_COMMON),
badges = parseBadgeList(cursor.requireBlob(RecipientTable.BADGES)),
needsPniSignature = cursor.requireBoolean(RecipientTable.NEEDS_PNI_SIGNATURE),
hiddenState = Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)),
callLinkRoomId = cursor.requireString(RecipientTable.CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) }
)
}
fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities {
val capabilities = cursor.requireLong(RecipientTable.CAPABILITIES)
return RecipientRecord.Capabilities(
rawBits = capabilities,
groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()),
senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()),
announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()),
changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()),
storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()),
giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()),
pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()),
paymentActivation = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH).toInt())
)
}
fun parseBadgeList(serializedBadgeList: ByteArray?): List<Badge> {
var badgeList: BadgeList? = null
if (serializedBadgeList != null) {
try {
badgeList = BadgeList.ADAPTER.decode(serializedBadgeList)
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, e)
}
}
val badges: List<Badge>
if (badgeList != null) {
val protoBadges = badgeList.badges
badges = ArrayList(protoBadges.size)
for (protoBadge in protoBadges) {
badges.add(Badges.fromDatabaseBadge(protoBadge))
}
} else {
badges = emptyList()
}
return badges
}
fun getSyncExtras(cursor: Cursor): RecipientRecord.SyncExtras {
val storageProtoRaw = cursor.optionalString(RecipientTable.STORAGE_SERVICE_PROTO).orElse(null)
val storageProto = if (storageProtoRaw != null) Base64.decodeOrThrow(storageProtoRaw) else null
val archived = cursor.optionalBoolean(ThreadTable.ARCHIVED).orElse(false)
val forcedUnread = cursor.optionalInt(ThreadTable.READ).map { status: Int -> status == ThreadTable.ReadStatus.FORCED_UNREAD.serialize() }.orElse(false)
val groupMasterKey = cursor.optionalBlob(GroupTable.V2_MASTER_KEY).map { GroupUtil.requireMasterKey(it) }.orElse(null)
val identityKey = cursor.optionalString(RecipientTable.IDENTITY_KEY).map { Base64.decodeOrThrow(it) }.orElse(null)
val identityStatus = cursor.optionalInt(RecipientTable.IDENTITY_STATUS).map { VerifiedStatus.forState(it) }.orElse(VerifiedStatus.DEFAULT)
val unregisteredTimestamp = cursor.optionalLong(RecipientTable.UNREGISTERED_TIMESTAMP).orElse(0)
val systemNickname = cursor.optionalString(RecipientTable.SYSTEM_NICKNAME).orElse(null)
return RecipientRecord.SyncExtras(
storageProto = storageProto,
groupMasterKey = groupMasterKey,
identityKey = identityKey,
identityStatus = identityStatus,
isArchived = archived,
isForcedUnread = forcedUnread,
unregisteredTimestamp = unregisteredTimestamp,
systemNickname = systemNickname
)
}
fun getExtras(cursor: Cursor): Recipient.Extras? {
return Recipient.Extras.from(getRecipientExtras(cursor))
}
fun getRecipientExtras(cursor: Cursor): RecipientExtras? {
return cursor.optionalBlob(RecipientTable.EXTRAS).map { b: ByteArray ->
try {
RecipientExtras.ADAPTER.decode(b)
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, e)
throw AssertionError(e)
}
}.orElse(null)
}
}

View File

@@ -1807,6 +1807,10 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
)
}
fun clearCache() {
threadIdCache.clear()
}
private fun createQuery(where: String, orderBy: String, offset: Long, limit: Long): String {
val projection = COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION.joinToString(separator = ",")
@@ -1879,7 +1883,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
open fun getCurrent(): ThreadRecord? {
val recipientId = RecipientId.from(cursor.requireLong(RECIPIENT_ID))
val recipientSettings = recipients.getRecord(context, cursor, RECIPIENT_ID)
val recipientSettings = RecipientTableCursorUtil.getRecord(context, cursor, RECIPIENT_ID)
val recipient: Recipient = if (recipientSettings.groupId != null) {
GroupTable.Reader(cursor).getCurrent()?.let { group ->

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.Base64;
@@ -51,11 +52,11 @@ public class IdentityKeyMismatch {
}
@JsonIgnore
public RecipientId getRecipientId(@NonNull Context context) {
public RecipientId getRecipientId() {
if (!TextUtils.isEmpty(recipientId)) {
return RecipientId.from(recipientId);
} else {
return Recipient.external(context, address).getId();
return Recipient.external(ApplicationDependencies.getApplication(), address).getId();
}
}

View File

@@ -8,6 +8,8 @@ import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -30,11 +32,11 @@ public class NetworkFailure {
public NetworkFailure() {}
@JsonIgnore
public RecipientId getRecipientId(@NonNull Context context) {
public RecipientId getRecipientId() {
if (!TextUtils.isEmpty(recipientId)) {
return RecipientId.from(recipientId);
} else {
return Recipient.external(context, address).getId();
return Recipient.external(ApplicationDependencies.getApplication(), address).getId();
}
}