Introduce ManyToMany table for group membership.
This commit is contained in:
parent
d635683303
commit
1b7e4e047c
11 changed files with 486 additions and 165 deletions
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
|
@ -48,8 +48,6 @@
|
||||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="HTML">
|
<codeStyleSettings language="HTML">
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
|
|
|
@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
|
||||||
SafetyNumberBottomSheet
|
SafetyNumberBottomSheet
|
||||||
.forIdentityRecordsAndDestinations(
|
.forIdentityRecordsAndDestinations(
|
||||||
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||||
destinations = listOf(ContactSearchKey.RecipientSearchKey.Story(myStoryRecipientId))
|
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
||||||
)
|
)
|
||||||
.show(conversationActivity.supportFragmentManager)
|
.show(conversationActivity.supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.delete
|
||||||
|
import org.signal.core.util.readToList
|
||||||
|
import org.signal.core.util.requireLong
|
||||||
|
import org.signal.core.util.withinTransaction
|
||||||
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.signal.storageservice.protos.groups.Member
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class GroupTableTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule()
|
||||||
|
|
||||||
|
private lateinit var groupTable: GroupTable
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
groupTable = SignalDatabase.groups
|
||||||
|
|
||||||
|
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
|
||||||
|
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whenICreateGroupV2_thenIExpectMemberRowsPopulated() {
|
||||||
|
val groupId = insertPushGroup()
|
||||||
|
|
||||||
|
//language=sql
|
||||||
|
val members: List<RecipientId> = groupTable.writableDatabase.query(
|
||||||
|
"""
|
||||||
|
SELECT ${GroupTable.MembershipTable.RECIPIENT_ID}
|
||||||
|
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||||
|
WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}"
|
||||||
|
""".trimIndent()
|
||||||
|
).readToList {
|
||||||
|
RecipientId.from(it.requireLong(GroupTable.RECIPIENT_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(2, members.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupV2_whenIGetGroupsContainingMember_thenIExpectGroup() {
|
||||||
|
val groupId = insertPushGroup()
|
||||||
|
insertThread(groupId)
|
||||||
|
|
||||||
|
val groups = groupTable.getGroupsContainingMember(harness.others[0], false)
|
||||||
|
|
||||||
|
assertEquals(1, groups.size)
|
||||||
|
assertEquals(groupId, groups[0].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAnMmsGroup_whenIGetMembers_thenIExpectAllMembers() {
|
||||||
|
val groupId = insertMmsGroup()
|
||||||
|
|
||||||
|
val groups = groupTable.getGroupMemberIds(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
|
||||||
|
|
||||||
|
assertEquals(2, groups.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
|
||||||
|
insertPushGroup()
|
||||||
|
insertMmsGroup(members = listOf(harness.others[1]))
|
||||||
|
|
||||||
|
val groups = groupTable.queryGroupsByMembership(
|
||||||
|
setOf(harness.self.id, harness.others[1]),
|
||||||
|
includeInactive = false,
|
||||||
|
excludeV1 = false,
|
||||||
|
excludeMms = false
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2, groups.cursor?.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
||||||
|
insertPushGroup()
|
||||||
|
insertMmsGroup(members = listOf(harness.others[1]))
|
||||||
|
|
||||||
|
val groups = groupTable.getGroups()
|
||||||
|
|
||||||
|
assertEquals(2, groups.cursor?.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroup_whenIGetGroup_thenIExpectGroup() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
insertThread(v2Group)
|
||||||
|
|
||||||
|
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||||
|
assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupAndARemap_whenIGetGroup_thenIExpectRemap() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
insertThread(v2Group)
|
||||||
|
|
||||||
|
groupTable.writableDatabase.withinTransaction {
|
||||||
|
RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||||
|
assertEquals(groupRecord.members.toSet(), setOf(harness.self.id, harness.others[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
|
||||||
|
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||||
|
|
||||||
|
assertTrue(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
|
||||||
|
groupTable.remove(v2Group, harness.others[0])
|
||||||
|
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||||
|
|
||||||
|
assertFalse(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
|
||||||
|
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1])
|
||||||
|
|
||||||
|
assertFalse(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
|
||||||
|
val v2Group = insertPushGroup()
|
||||||
|
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
|
||||||
|
val groupRecord = groupTable.getGroup(v2Group)
|
||||||
|
|
||||||
|
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||||
|
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
|
||||||
|
val other = insertMmsGroup(members + listOf(harness.others[1]))
|
||||||
|
val mmsGroup = insertMmsGroup(members)
|
||||||
|
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
|
||||||
|
|
||||||
|
assertNotEquals(other, actual)
|
||||||
|
assertEquals(mmsGroup, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertThread(groupId: GroupId): Long {
|
||||||
|
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||||
|
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertMmsGroup(members: List<RecipientId> = listOf(harness.self.id, harness.others[0])): GroupId {
|
||||||
|
val id = GroupId.createMms(SecureRandom())
|
||||||
|
groupTable.create(
|
||||||
|
id,
|
||||||
|
null,
|
||||||
|
members.apply {
|
||||||
|
println("Creating a group with ${members.size} members")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertPushGroup(): GroupId {
|
||||||
|
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||||
|
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||||
|
.addAllMembers(
|
||||||
|
listOf(
|
||||||
|
DecryptedMember.newBuilder()
|
||||||
|
.setUuid(harness.self.requireServiceId().toByteString())
|
||||||
|
.setJoinedAtRevision(0)
|
||||||
|
.setRole(Member.Role.DEFAULT)
|
||||||
|
.build(),
|
||||||
|
DecryptedMember.newBuilder()
|
||||||
|
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
|
||||||
|
.setJoinedAtRevision(0)
|
||||||
|
.setRole(Member.Role.DEFAULT)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setRevision(0)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return groupTable.create(groupMasterKey, decryptedGroupState)
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||||
val recipients = harness.others
|
val recipients = harness.others
|
||||||
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
|
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||||
|
|
||||||
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
|
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||||
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val recipients = harness.others
|
val recipients = harness.others
|
||||||
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
|
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
|
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
|
||||||
|
@ -59,7 +59,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val distributionListMembers = harness.others.take(5)
|
val distributionListMembers = harness.others.take(5)
|
||||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
|
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
|
||||||
|
@ -82,7 +82,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||||
val distributionListMembers = harness.others.take(5)
|
val distributionListMembers = harness.others.take(5)
|
||||||
val toRemove = distributionListMembers.last()
|
val toRemove = distributionListMembers.last()
|
||||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val distributionListMembers = harness.others.take(5)
|
val distributionListMembers = harness.others.take(5)
|
||||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.signal.core.util.SqlUtil.appendArg
|
||||||
import org.signal.core.util.SqlUtil.buildArgs
|
import org.signal.core.util.SqlUtil.buildArgs
|
||||||
import org.signal.core.util.SqlUtil.buildCaseInsensitiveGlobPattern
|
import org.signal.core.util.SqlUtil.buildCaseInsensitiveGlobPattern
|
||||||
import org.signal.core.util.SqlUtil.buildCollectionQuery
|
import org.signal.core.util.SqlUtil.buildCollectionQuery
|
||||||
|
import org.signal.core.util.delete
|
||||||
import org.signal.core.util.exists
|
import org.signal.core.util.exists
|
||||||
import org.signal.core.util.isAbsent
|
import org.signal.core.util.isAbsent
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
@ -50,7 +51,6 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.Util
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
|
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||||
|
@ -58,11 +58,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.lang.AssertionError
|
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
@ -72,12 +68,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(GroupTable::class.java)
|
private val TAG = Log.tag(GroupTable::class.java)
|
||||||
|
|
||||||
|
const val MEMBER_GROUP_CONCAT = "member_group_concat"
|
||||||
|
|
||||||
const val TABLE_NAME = "groups"
|
const val TABLE_NAME = "groups"
|
||||||
const val ID = "_id"
|
const val ID = "_id"
|
||||||
const val GROUP_ID = "group_id"
|
const val GROUP_ID = "group_id"
|
||||||
const val RECIPIENT_ID = "recipient_id"
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
const val TITLE = "title"
|
const val TITLE = "title"
|
||||||
const val MEMBERS = "members"
|
|
||||||
const val AVATAR_ID = "avatar_id"
|
const val AVATAR_ID = "avatar_id"
|
||||||
const val AVATAR_KEY = "avatar_key"
|
const val AVATAR_KEY = "avatar_key"
|
||||||
const val AVATAR_CONTENT_TYPE = "avatar_content_type"
|
const val AVATAR_CONTENT_TYPE = "avatar_content_type"
|
||||||
|
@ -112,7 +109,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
$GROUP_ID TEXT,
|
$GROUP_ID TEXT,
|
||||||
$RECIPIENT_ID INTEGER,
|
$RECIPIENT_ID INTEGER,
|
||||||
$TITLE TEXT,
|
$TITLE TEXT,
|
||||||
$MEMBERS TEXT,
|
|
||||||
$AVATAR_ID INTEGER,
|
$AVATAR_ID INTEGER,
|
||||||
$AVATAR_KEY BLOB,
|
$AVATAR_KEY BLOB,
|
||||||
$AVATAR_CONTENT_TYPE TEXT,
|
$AVATAR_CONTENT_TYPE TEXT,
|
||||||
|
@ -145,7 +141,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
GROUP_ID,
|
GROUP_ID,
|
||||||
RECIPIENT_ID,
|
RECIPIENT_ID,
|
||||||
TITLE,
|
TITLE,
|
||||||
MEMBERS,
|
|
||||||
UNMIGRATED_V1_MEMBERS,
|
UNMIGRATED_V1_MEMBERS,
|
||||||
AVATAR_ID,
|
AVATAR_ID,
|
||||||
AVATAR_KEY,
|
AVATAR_KEY,
|
||||||
|
@ -165,43 +160,77 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
.filterNot { it == RECIPIENT_ID }
|
.filterNot { it == RECIPIENT_ID }
|
||||||
.map { columnName: String -> "$TABLE_NAME.$columnName" }
|
.map { columnName: String -> "$TABLE_NAME.$columnName" }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
|
//language=sql
|
||||||
|
private val JOINED_GROUP_SELECT = """
|
||||||
|
SELECT
|
||||||
|
DISTINCT $TABLE_NAME.*,
|
||||||
|
GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}) as $MEMBER_GROUP_CONCAT
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
INNER JOIN ${MembershipTable.TABLE_NAME} ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||||
|
""".toSingleLine()
|
||||||
|
|
||||||
|
val CREATE_TABLES = arrayOf(CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MembershipTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
|
||||||
|
companion object {
|
||||||
|
const val TABLE_NAME = "group_membership"
|
||||||
|
|
||||||
|
const val ID = "_id"
|
||||||
|
const val GROUP_ID = "group_id"
|
||||||
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
|
|
||||||
|
//language=sql
|
||||||
|
@JvmField
|
||||||
|
val CREATE_TABLE = """
|
||||||
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
$ID INTEGER PRIMARY KEY,
|
||||||
|
$GROUP_ID TEXT NOT NULL,
|
||||||
|
$RECIPIENT_ID INTEGER NOT NULL,
|
||||||
|
UNIQUE($GROUP_ID, $RECIPIENT_ID)
|
||||||
|
)
|
||||||
|
""".toSingleLine()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroup(recipientId: RecipientId): Optional<GroupRecord> {
|
fun getGroup(recipientId: RecipientId): Optional<GroupRecord> {
|
||||||
readableDatabase
|
return getGroup(SqlUtil.Query("$TABLE_NAME.$RECIPIENT_ID = ?", buildArgs(recipientId)))
|
||||||
.select()
|
|
||||||
.from(TABLE_NAME)
|
|
||||||
.where("$RECIPIENT_ID = ?", recipientId)
|
|
||||||
.run()
|
|
||||||
.use { cursor ->
|
|
||||||
return if (cursor.moveToFirst()) {
|
|
||||||
getGroup(cursor)
|
|
||||||
} else {
|
|
||||||
Optional.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroup(groupId: GroupId): Optional<GroupRecord> {
|
fun getGroup(groupId: GroupId): Optional<GroupRecord> {
|
||||||
|
return getGroup(SqlUtil.Query("$TABLE_NAME.$GROUP_ID = ?", buildArgs(groupId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGroup(query: SqlUtil.Query): Optional<GroupRecord> {
|
||||||
|
//language=sql
|
||||||
|
val select = "$JOINED_GROUP_SELECT WHERE ${query.where} GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}"
|
||||||
|
|
||||||
readableDatabase
|
readableDatabase
|
||||||
.select()
|
.query(select, query.whereArgs)
|
||||||
.from(TABLE_NAME)
|
|
||||||
.where("$GROUP_ID = ?", groupId.toString())
|
|
||||||
.run()
|
|
||||||
.use { cursor ->
|
.use { cursor ->
|
||||||
return if (cursor.moveToFirst()) {
|
return if (cursor.moveToFirst()) {
|
||||||
val groupRecord = getGroup(cursor)
|
val groupRecord = getGroup(cursor)
|
||||||
if (groupRecord.isPresent && RemappedRecords.getInstance().areAnyRemapped(groupRecord.get().members)) {
|
if (groupRecord.isPresent && RemappedRecords.getInstance().areAnyRemapped(groupRecord.get().members)) {
|
||||||
|
val groupId = groupRecord.get().id
|
||||||
val remaps = RemappedRecords.getInstance().buildRemapDescription(groupRecord.get().members)
|
val remaps = RemappedRecords.getInstance().buildRemapDescription(groupRecord.get().members)
|
||||||
Log.w(TAG, "Found a group with remapped recipients in it's membership list! Updating the list. GroupId: $groupId, Remaps: $remaps", true)
|
Log.w(TAG, "Found a group with remapped recipients in it's membership list! Updating the list. GroupId: $groupId, Remaps: $remaps", true)
|
||||||
|
|
||||||
val remapped: Collection<RecipientId> = RemappedRecords.getInstance().remap(groupRecord.get().members)
|
val oldToNew: List<Pair<RecipientId, RecipientId?>> = groupRecord.get().members.map {
|
||||||
|
it to RemappedRecords.getInstance().getRecipient(it).orElse(null)
|
||||||
|
}.filterNot { (old, new) -> new == null || old == new }
|
||||||
|
|
||||||
val updateCount = writableDatabase
|
var updateCount = 0
|
||||||
.update(TABLE_NAME)
|
if (oldToNew.isNotEmpty()) {
|
||||||
.values(MEMBERS to remapped.serialize())
|
writableDatabase.withinTransaction { db ->
|
||||||
.where("$GROUP_ID = ?", groupId)
|
for ((old, new) in oldToNew) {
|
||||||
.run()
|
updateCount += db.update(MembershipTable.TABLE_NAME)
|
||||||
|
.values(MembershipTable.RECIPIENT_ID to new!!.serialize())
|
||||||
|
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, old)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateCount > 0) {
|
if (updateCount > 0) {
|
||||||
getGroup(groupId)
|
getGroup(groupId)
|
||||||
|
@ -240,33 +269,11 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
* @return A gv1 group whose expected v2 ID matches the one provided.
|
* @return A gv1 group whose expected v2 ID matches the one provided.
|
||||||
*/
|
*/
|
||||||
fun getGroupV1ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
|
fun getGroupV1ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
|
||||||
readableDatabase
|
return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ?", buildArgs(gv2Id)))
|
||||||
.select(*GROUP_PROJECTION)
|
|
||||||
.from(TABLE_NAME)
|
|
||||||
.where("$EXPECTED_V2_ID = ?", gv2Id)
|
|
||||||
.run()
|
|
||||||
.use { cursor ->
|
|
||||||
return if (cursor.moveToFirst()) {
|
|
||||||
getGroup(cursor)
|
|
||||||
} else {
|
|
||||||
Optional.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroupByDistributionId(distributionId: DistributionId): Optional<GroupRecord> {
|
fun getGroupByDistributionId(distributionId: DistributionId): Optional<GroupRecord> {
|
||||||
readableDatabase
|
return getGroup(SqlUtil.Query("$TABLE_NAME.$DISTRIBUTION_ID = ?", buildArgs(distributionId)))
|
||||||
.select()
|
|
||||||
.from(TABLE_NAME)
|
|
||||||
.where("$DISTRIBUTION_ID = ?", distributionId)
|
|
||||||
.run()
|
|
||||||
.use { cursor ->
|
|
||||||
return if (cursor.moveToFirst()) {
|
|
||||||
getGroup(cursor)
|
|
||||||
} else {
|
|
||||||
Optional.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnmigratedV1Members(id: GroupId.V2) {
|
fun removeUnmigratedV1Members(id: GroupId.V2) {
|
||||||
|
@ -338,7 +345,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
|
|
||||||
fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
|
fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
|
||||||
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
||||||
val cursor = databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, "$TITLE COLLATE NOCASE ASC")
|
val statement = """
|
||||||
|
$JOINED_GROUP_SELECT
|
||||||
|
WHERE ${query.where}
|
||||||
|
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||||
|
ORDER BY $TITLE COLLATE NOCASE ASC
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cursor = databaseHelper.signalReadableDatabase.query(statement, query.whereArgs)
|
||||||
return Reader(cursor)
|
return Reader(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,21 +367,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
recipientIds = recipientIds.take(30).toSet()
|
recipientIds = recipientIds.take(30).toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
val recipientLikeClauses = recipientIds
|
val membershipQuery = SqlUtil.buildSingleCollectionQuery("${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}", recipientIds)
|
||||||
.map { it.toLong() }
|
|
||||||
.map { id -> "($MEMBERS LIKE $id || ',%' OR $MEMBERS LIKE '%,' || $id || ',%' OR $MEMBERS LIKE '%,' || $id)" }
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
var query: String
|
var query: String
|
||||||
val queryArgs: Array<String>
|
val queryArgs: Array<String>
|
||||||
val membershipQuery = "(" + Util.join(recipientLikeClauses, " OR ") + ")"
|
|
||||||
|
|
||||||
if (includeInactive) {
|
if (includeInactive) {
|
||||||
query = "$membershipQuery AND ($ACTIVE = ? OR $RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
query = "${membershipQuery.where} AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||||
queryArgs = buildArgs(1)
|
queryArgs = membershipQuery.whereArgs + buildArgs(1)
|
||||||
} else {
|
} else {
|
||||||
query = "$membershipQuery AND $ACTIVE = ?"
|
query = "${membershipQuery.where} AND $ACTIVE = ?"
|
||||||
queryArgs = buildArgs(1)
|
queryArgs = membershipQuery.whereArgs + buildArgs(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (excludeV1) {
|
if (excludeV1) {
|
||||||
|
@ -378,15 +388,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
query += " AND $MMS = 0"
|
query += " AND $MMS = 0"
|
||||||
}
|
}
|
||||||
|
|
||||||
return Reader(readableDatabase.query(TABLE_NAME, null, query, queryArgs, null, null, null))
|
return Reader(readableDatabase.query("$JOINED_GROUP_SELECT WHERE $query GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}", queryArgs))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun queryGroupsByRecency(groupQuery: GroupQuery): Reader {
|
private fun queryGroupsByRecency(groupQuery: GroupQuery): Reader {
|
||||||
val query = getGroupQueryWhereStatement(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
|
val query = getGroupQueryWhereStatement(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
|
||||||
val sql = """
|
val sql = """
|
||||||
SELECT $TABLE_NAME.*
|
$JOINED_GROUP_SELECT
|
||||||
FROM $TABLE_NAME LEFT JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
|
||||||
WHERE ${query.where}
|
WHERE ${query.where}
|
||||||
|
${"GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}"}
|
||||||
ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC
|
ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC
|
||||||
""".toSingleLine()
|
""".toSingleLine()
|
||||||
|
|
||||||
|
@ -407,7 +417,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern(inputQuery)
|
val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern(inputQuery)
|
||||||
|
|
||||||
if (includeInactive) {
|
if (includeInactive) {
|
||||||
query = "$TITLE GLOB ? AND ($ACTIVE = ? OR $RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
query = "$TITLE GLOB ? AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||||
queryArgs = buildArgs(caseInsensitiveQuery, 1)
|
queryArgs = buildArgs(caseInsensitiveQuery, 1)
|
||||||
} else {
|
} else {
|
||||||
query = "$TITLE GLOB ? AND $ACTIVE = ?"
|
query = "$TITLE GLOB ? AND $ACTIVE = ?"
|
||||||
|
@ -456,23 +466,27 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOrCreateMmsGroupForMembers(members: List<RecipientId>): GroupId.Mms {
|
fun getOrCreateMmsGroupForMembers(members: Set<RecipientId>): GroupId.Mms {
|
||||||
val sortedMembers = members.sorted()
|
//language=sql
|
||||||
|
val statement = """
|
||||||
|
SELECT ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} as gid
|
||||||
|
FROM ${MembershipTable.TABLE_NAME}
|
||||||
|
INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||||
|
WHERE ${MembershipTable.TABLE_NAME}.$RECIPIENT_ID IN (${members.joinToString(",") { it.serialize() }}) AND $TABLE_NAME.$MMS = 1
|
||||||
|
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||||
|
HAVING (SELECT COUNT(*) FROM ${MembershipTable.TABLE_NAME} WHERE ${MembershipTable.GROUP_ID} = gid) = ${members.size}
|
||||||
|
ORDER BY ${MembershipTable.TABLE_NAME}.${MembershipTable.ID} ASC
|
||||||
|
""".toSingleLine()
|
||||||
|
|
||||||
readableDatabase
|
return readableDatabase.query(statement).use { cursor ->
|
||||||
.select(GROUP_ID)
|
if (cursor.moveToNext()) {
|
||||||
.from(TABLE_NAME)
|
return GroupId.parseOrThrow(cursor.requireNonNullString("gid")).requireMms()
|
||||||
.where("$MEMBERS = ? AND $MMS = ?", sortedMembers.serialize(), 1)
|
} else {
|
||||||
.run()
|
val groupId = GroupId.createMms(SecureRandom())
|
||||||
.use { cursor ->
|
create(groupId, null, members)
|
||||||
return if (cursor.moveToNext()) {
|
groupId
|
||||||
GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)).requireMms()
|
|
||||||
} else {
|
|
||||||
val groupId = GroupId.createMms(SecureRandom())
|
|
||||||
create(groupId, null, sortedMembers)
|
|
||||||
groupId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@ -493,9 +507,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun getGroupsContainingMember(recipientId: RecipientId, pushOnly: Boolean, includeInactive: Boolean): List<GroupRecord> {
|
fun getGroupsContainingMember(recipientId: RecipientId, pushOnly: Boolean, includeInactive: Boolean): List<GroupRecord> {
|
||||||
val table = "$TABLE_NAME INNER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}"
|
//language=sql
|
||||||
var query = "$MEMBERS LIKE ?"
|
val table = """
|
||||||
var args = buildArgs("%${recipientId.serialize()}%")
|
$JOINED_GROUP_SELECT
|
||||||
|
INNER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
||||||
|
""".toSingleLine()
|
||||||
|
|
||||||
|
var query = "${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} = ?"
|
||||||
|
var args = buildArgs(recipientId)
|
||||||
val orderBy = "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC"
|
val orderBy = "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC"
|
||||||
|
|
||||||
if (pushOnly) {
|
if (pushOnly) {
|
||||||
|
@ -509,23 +528,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
|
|
||||||
return readableDatabase
|
return readableDatabase
|
||||||
.query(table, null, query, args, null, null, orderBy)
|
.query("$table WHERE $query GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} ORDER BY $orderBy".apply { println(this) }, args)
|
||||||
.readToList { cursor ->
|
.readToList { cursor ->
|
||||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
getGroup(cursor).get()
|
||||||
if (RecipientId.serializedListContains(serializedMembers, recipientId)) {
|
|
||||||
getGroup(cursor).get()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.filterNotNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroups(): Reader {
|
fun getGroups(): Reader {
|
||||||
val cursor = readableDatabase
|
val cursor = readableDatabase.query("$JOINED_GROUP_SELECT GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}")
|
||||||
.select()
|
|
||||||
.from(TABLE_NAME)
|
|
||||||
.run()
|
|
||||||
return Reader(cursor)
|
return Reader(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -648,6 +658,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
groupMasterKey: GroupMasterKey?,
|
groupMasterKey: GroupMasterKey?,
|
||||||
groupState: DecryptedGroup?
|
groupState: DecryptedGroup?
|
||||||
) {
|
) {
|
||||||
|
val membershipValues = mutableListOf<ContentValues>()
|
||||||
val groupRecipientId = recipients.getOrInsertFromGroupId(groupId)
|
val groupRecipientId = recipients.getOrInsertFromGroupId(groupId)
|
||||||
val members: List<RecipientId> = memberCollection.toSet().sorted()
|
val members: List<RecipientId> = memberCollection.toSet().sorted()
|
||||||
var groupMembers: List<RecipientId> = members
|
var groupMembers: List<RecipientId> = members
|
||||||
|
@ -657,7 +668,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
values.put(RECIPIENT_ID, groupRecipientId.serialize())
|
values.put(RECIPIENT_ID, groupRecipientId.serialize())
|
||||||
values.put(GROUP_ID, groupId.toString())
|
values.put(GROUP_ID, groupId.toString())
|
||||||
values.put(TITLE, title)
|
values.put(TITLE, title)
|
||||||
values.put(MEMBERS, members.serialize())
|
membershipValues.addAll(members.toContentValues(groupId))
|
||||||
values.put(MMS, groupId.isMms)
|
values.put(MMS, groupId.isMms)
|
||||||
|
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
|
@ -693,14 +704,25 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
values.put(V2_MASTER_KEY, groupMasterKey.serialize())
|
values.put(V2_MASTER_KEY, groupMasterKey.serialize())
|
||||||
values.put(V2_REVISION, groupState.revision)
|
values.put(V2_REVISION, groupState.revision)
|
||||||
values.put(V2_DECRYPTED_GROUP, groupState.toByteArray())
|
values.put(V2_DECRYPTED_GROUP, groupState.toByteArray())
|
||||||
values.put(MEMBERS, groupMembers.serialize())
|
membershipValues.clear()
|
||||||
|
membershipValues.addAll(groupMembers.toContentValues(groupId))
|
||||||
} else {
|
} else {
|
||||||
if (groupId.isV2) {
|
if (groupId.isV2) {
|
||||||
throw AssertionError("V2 group id but no master key")
|
throw AssertionError("V2 group id but no master key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writableDatabase.insert(TABLE_NAME, null, values)
|
writableDatabase.withinTransaction { db ->
|
||||||
|
db.insert(TABLE_NAME, null, values)
|
||||||
|
SqlUtil.buildBulkInsert(
|
||||||
|
MembershipTable.TABLE_NAME,
|
||||||
|
arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID),
|
||||||
|
membershipValues
|
||||||
|
)
|
||||||
|
.forEach {
|
||||||
|
db.execSQL(it.where, it.whereArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (groupState != null && groupState.hasDisappearingMessagesTimer()) {
|
if (groupState != null && groupState.hasDisappearingMessagesTimer()) {
|
||||||
recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer.duration)
|
recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer.duration)
|
||||||
|
@ -829,7 +851,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupMembers = getV2GroupMembers(decryptedGroup, true)
|
val groupMembers = getV2GroupMembers(decryptedGroup, true)
|
||||||
contentValues.put(MEMBERS, groupMembers.serialize())
|
|
||||||
|
|
||||||
if (existingGroup.isPresent && existingGroup.get().isV2Group) {
|
if (existingGroup.isPresent && existingGroup.get().isV2Group) {
|
||||||
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
|
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
|
||||||
|
@ -842,11 +863,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writableDatabase
|
writableDatabase.withinTransaction { database ->
|
||||||
.update(TABLE_NAME)
|
database
|
||||||
.values(contentValues)
|
.update(TABLE_NAME)
|
||||||
.where("$GROUP_ID = ?", groupId.toString())
|
.values(contentValues)
|
||||||
.run()
|
.where("$GROUP_ID = ?", groupId.toString())
|
||||||
|
.run()
|
||||||
|
|
||||||
|
performMembershipUpdate(database, groupId, groupMembers)
|
||||||
|
}
|
||||||
|
|
||||||
if (decryptedGroup.hasDisappearingMessagesTimer()) {
|
if (decryptedGroup.hasDisappearingMessagesTimer()) {
|
||||||
recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer.duration)
|
recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer.duration)
|
||||||
|
@ -898,27 +923,24 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMembers(groupId: GroupId, members: List<RecipientId>) {
|
fun updateMembers(groupId: GroupId, members: List<RecipientId>) {
|
||||||
writableDatabase
|
writableDatabase.withinTransaction { database ->
|
||||||
.update(TABLE_NAME)
|
database
|
||||||
.values(
|
.update(TABLE_NAME)
|
||||||
MEMBERS to members.sorted().serialize(),
|
.values(ACTIVE to 1)
|
||||||
ACTIVE to 1
|
.where("$GROUP_ID = ?", groupId)
|
||||||
)
|
.run()
|
||||||
.where("$GROUP_ID = ?", groupId)
|
|
||||||
.run()
|
performMembershipUpdate(database, groupId, members)
|
||||||
|
}
|
||||||
|
|
||||||
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
||||||
Recipient.live(groupRecipient).refresh()
|
Recipient.live(groupRecipient).refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(groupId: GroupId, source: RecipientId) {
|
fun remove(groupId: GroupId, source: RecipientId) {
|
||||||
val currentMembers: MutableList<RecipientId> = getCurrentMembers(groupId)
|
|
||||||
currentMembers -= source
|
|
||||||
|
|
||||||
writableDatabase
|
writableDatabase
|
||||||
.update(TABLE_NAME)
|
.delete(MembershipTable.TABLE_NAME)
|
||||||
.values(MEMBERS to currentMembers.serialize())
|
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, source)
|
||||||
.where("$GROUP_ID = ?", groupId)
|
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
||||||
|
@ -927,17 +949,34 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
|
|
||||||
private fun getCurrentMembers(groupId: GroupId): MutableList<RecipientId> {
|
private fun getCurrentMembers(groupId: GroupId): MutableList<RecipientId> {
|
||||||
return readableDatabase
|
return readableDatabase
|
||||||
.select(MEMBERS)
|
.select(MembershipTable.RECIPIENT_ID)
|
||||||
.from(TABLE_NAME)
|
.from(MembershipTable.TABLE_NAME)
|
||||||
.where("$GROUP_ID = ?", groupId)
|
.where("${MembershipTable.GROUP_ID} = ?", groupId)
|
||||||
.run()
|
.run()
|
||||||
.readToList { cursor ->
|
.readToList { cursor ->
|
||||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID))
|
||||||
return RecipientId.fromSerializedList(serializedMembers)
|
|
||||||
}
|
}
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>) {
|
||||||
|
check(database.inTransaction())
|
||||||
|
database
|
||||||
|
.delete(MembershipTable.TABLE_NAME)
|
||||||
|
.where("${MembershipTable.GROUP_ID} = ?", groupId)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
val inserts = SqlUtil.buildBulkInsert(
|
||||||
|
MembershipTable.TABLE_NAME,
|
||||||
|
arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID),
|
||||||
|
members.toContentValues(groupId)
|
||||||
|
)
|
||||||
|
|
||||||
|
inserts.forEach {
|
||||||
|
database.execSQL(it.where, it.whereArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isActive(groupId: GroupId): Boolean {
|
fun isActive(groupId: GroupId): Boolean {
|
||||||
val record = getGroup(groupId)
|
val record = getGroup(groupId)
|
||||||
return record.isPresent && record.get().isActive
|
return record.isPresent && record.get().isActive
|
||||||
|
@ -961,19 +1000,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean {
|
fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean {
|
||||||
readableDatabase
|
return readableDatabase
|
||||||
.select(MEMBERS)
|
.exists(MembershipTable.TABLE_NAME)
|
||||||
.from(TABLE_NAME)
|
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
|
||||||
.where("$GROUP_ID = ?", groupId)
|
|
||||||
.run()
|
.run()
|
||||||
.use { cursor ->
|
|
||||||
return if (cursor.moveToFirst()) {
|
|
||||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
|
||||||
RecipientId.serializedListContains(serializedMembers, recipientId)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllGroupV2Ids(): List<GroupId.V2> {
|
fun getAllGroupV2Ids(): List<GroupId.V2> {
|
||||||
|
@ -1005,15 +1035,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
||||||
|
writableDatabase
|
||||||
|
.update(MembershipTable.TABLE_NAME)
|
||||||
|
.values(RECIPIENT_ID to toId.serialize())
|
||||||
|
.where("${MembershipTable.RECIPIENT_ID} = ?", fromId)
|
||||||
|
.run()
|
||||||
|
|
||||||
for (group in getGroupsContainingMember(fromId, false, true)) {
|
for (group in getGroupsContainingMember(fromId, false, true)) {
|
||||||
val newMembers: Set<RecipientId> = group.members.toSet() - fromId + toId
|
|
||||||
|
|
||||||
writableDatabase
|
|
||||||
.update(TABLE_NAME)
|
|
||||||
.values(MEMBERS to newMembers.serialize())
|
|
||||||
.where("$RECIPIENT_ID = ?", group.recipientId)
|
|
||||||
.run()
|
|
||||||
|
|
||||||
if (group.isV2Group) {
|
if (group.isV2Group) {
|
||||||
removeUnmigratedV1Members(group.id.requireV2(), listOf(fromId))
|
removeUnmigratedV1Members(group.id.requireV2(), listOf(fromId))
|
||||||
}
|
}
|
||||||
|
@ -1042,7 +1070,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
id = GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)),
|
id = GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)),
|
||||||
recipientId = RecipientId.from(cursor.requireNonNullString(RECIPIENT_ID)),
|
recipientId = RecipientId.from(cursor.requireNonNullString(RECIPIENT_ID)),
|
||||||
title = cursor.requireString(TITLE),
|
title = cursor.requireString(TITLE),
|
||||||
serializedMembers = cursor.requireString(MEMBERS),
|
serializedMembers = cursor.requireString(MEMBER_GROUP_CONCAT),
|
||||||
serializedUnmigratedV1Members = cursor.requireString(UNMIGRATED_V1_MEMBERS),
|
serializedUnmigratedV1Members = cursor.requireString(UNMIGRATED_V1_MEMBERS),
|
||||||
avatarId = cursor.requireLong(AVATAR_ID),
|
avatarId = cursor.requireLong(AVATAR_ID),
|
||||||
avatarKey = cursor.requireBlob(AVATAR_KEY),
|
avatarKey = cursor.requireBlob(AVATAR_KEY),
|
||||||
|
@ -1252,6 +1280,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||||
return RecipientId.toSerializedList(this)
|
return RecipientId.toSerializedList(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Collection<RecipientId>.toContentValues(groupId: GroupId): List<ContentValues> {
|
||||||
|
return map {
|
||||||
|
contentValuesOf(
|
||||||
|
MembershipTable.GROUP_ID to groupId.serialize(),
|
||||||
|
MembershipTable.RECIPIENT_ID to it.serialize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun uuidsToRecipientIds(uuids: List<UUID>): MutableList<RecipientId> {
|
private fun uuidsToRecipientIds(uuids: List<UUID>): MutableList<RecipientId> {
|
||||||
return uuids
|
return uuids
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
|
|
@ -86,7 +86,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
db.execSQL(IdentityTable.CREATE_TABLE)
|
db.execSQL(IdentityTable.CREATE_TABLE)
|
||||||
db.execSQL(DraftTable.CREATE_TABLE)
|
db.execSQL(DraftTable.CREATE_TABLE)
|
||||||
db.execSQL(PushTable.CREATE_TABLE)
|
db.execSQL(PushTable.CREATE_TABLE)
|
||||||
db.execSQL(GroupTable.CREATE_TABLE)
|
executeStatements(db, GroupTable.CREATE_TABLES)
|
||||||
db.execSQL(RecipientTable.CREATE_TABLE)
|
db.execSQL(RecipientTable.CREATE_TABLE)
|
||||||
db.execSQL(GroupReceiptTable.CREATE_TABLE)
|
db.execSQL(GroupReceiptTable.CREATE_TABLE)
|
||||||
db.execSQL(OneTimePreKeyTable.CREATE_TABLE)
|
db.execSQL(OneTimePreKeyTable.CREATE_TABLE)
|
||||||
|
|
|
@ -1610,12 +1610,17 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||||
private fun createQuery(where: String, orderBy: String, offset: Long, limit: Long): String {
|
private fun createQuery(where: String, orderBy: String, offset: Long, limit: Long): String {
|
||||||
val projection = COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION.joinToString(separator = ",")
|
val projection = COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION.joinToString(separator = ",")
|
||||||
|
|
||||||
|
//language=sql
|
||||||
var query = """
|
var query = """
|
||||||
SELECT $projection
|
SELECT $projection, ${GroupTable.MEMBER_GROUP_CONCAT}
|
||||||
FROM $TABLE_NAME
|
FROM $TABLE_NAME
|
||||||
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||||
LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
||||||
WHERE $where
|
LEFT OUTER JOIN (
|
||||||
|
SELECT group_id, GROUP_CONCAT(${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID}) as ${GroupTable.MEMBER_GROUP_CONCAT}
|
||||||
|
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||||
|
) as MembershipAlias ON MembershipAlias.${GroupTable.MembershipTable.GROUP_ID} = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
|
||||||
|
WHERE $where
|
||||||
ORDER BY $orderBy
|
ORDER BY $orderBy
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V168_SingleMessageT
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V169_EmojiSearchIndexRank
|
import org.thoughtcrime.securesms.database.helpers.migration.V169_EmojiSearchIndexRank
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V170_CallTableMigration
|
import org.thoughtcrime.securesms.database.helpers.migration.V170_CallTableMigration
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V171_ThreadForeignKeyFix
|
import org.thoughtcrime.securesms.database.helpers.migration.V171_ThreadForeignKeyFix
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.migration.V172_GroupMembershipMigration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||||
|
@ -35,7 +36,7 @@ object SignalDatabaseMigrations {
|
||||||
|
|
||||||
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
||||||
|
|
||||||
const val DATABASE_VERSION = 171
|
const val DATABASE_VERSION = 172
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -130,6 +131,10 @@ object SignalDatabaseMigrations {
|
||||||
if (oldVersion < 171) {
|
if (oldVersion < 171) {
|
||||||
V171_ThreadForeignKeyFix.migrate(context, db, oldVersion, newVersion)
|
V171_ThreadForeignKeyFix.migrate(context, db, oldVersion, newVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 172) {
|
||||||
|
V172_GroupMembershipMigration.migrate(context, db, oldVersion, newVersion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package org.thoughtcrime.securesms.database.helpers.migration
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.readToList
|
||||||
|
import org.signal.core.util.requireNonNullString
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates all IDs from the GroupTable into the GroupMembershipTable
|
||||||
|
*/
|
||||||
|
@Suppress("ClassName")
|
||||||
|
object V172_GroupMembershipMigration : SignalDatabaseMigration {
|
||||||
|
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE group_membership (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
group_id TEXT NOT NULL,
|
||||||
|
recipient_id INTEGER NOT NULL,
|
||||||
|
UNIQUE(group_id, recipient_id)
|
||||||
|
);
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
//language=sql
|
||||||
|
val total = db.query("SELECT COUNT(*) FROM groups").use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
it.getInt(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(0..total).chunked(500).forEachIndexed { index, _ ->
|
||||||
|
//language=sql
|
||||||
|
val groupIdToMembers: List<Pair<String, List<Long>>> = db.query("SELECT members, group_id FROM groups LIMIT 500 OFFSET ${index * 500}").readToList { cursor ->
|
||||||
|
val groupId = cursor.requireNonNullString("group_id")
|
||||||
|
val members: List<Long> = cursor.requireNonNullString("members").split(",").filterNot { it.isEmpty() }.map { it.toLong() }
|
||||||
|
|
||||||
|
groupId to members
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((group_id, members) in groupIdToMembers) {
|
||||||
|
val queries = SqlUtil.buildBulkInsert(
|
||||||
|
"group_membership",
|
||||||
|
arrayOf("group_id", "recipient_id"),
|
||||||
|
members.map {
|
||||||
|
contentValuesOf(
|
||||||
|
"group_id" to group_id,
|
||||||
|
"recipient_id" to it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (query in queries) {
|
||||||
|
db.execSQL(query.where, query.whereArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.execSQL("ALTER TABLE groups DROP COLUMN members")
|
||||||
|
}
|
||||||
|
}
|
|
@ -245,7 +245,7 @@ public class MmsDownloadJob extends BaseJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members.size() > 2) {
|
if (members.size() > 2) {
|
||||||
List<RecipientId> recipients = new ArrayList<>(members);
|
Set<RecipientId> recipients = new HashSet<>(members);
|
||||||
group = Optional.of(SignalDatabase.groups().getOrCreateMmsGroupForMembers(recipients));
|
group = Optional.of(SignalDatabase.groups().getOrCreateMmsGroupForMembers(recipients));
|
||||||
}
|
}
|
||||||
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, TimeUnit.SECONDS.toMillis(retrieved.getDate()), -1, System.currentTimeMillis(), attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts), false, false);
|
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, TimeUnit.SECONDS.toMillis(retrieved.getDate()), -1, System.currentTimeMillis(), attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts), false, false);
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.signal.core.util.requireBlob
|
import org.signal.core.util.requireBlob
|
||||||
import org.signal.core.util.requireString
|
|
||||||
import org.signal.spinner.ColumnTransformer
|
import org.signal.spinner.ColumnTransformer
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
|
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||||
|
@ -14,7 +13,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
|
||||||
object GV2Transformer : ColumnTransformer {
|
object GV2Transformer : ColumnTransformer {
|
||||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||||
return columnName == GroupTable.V2_DECRYPTED_GROUP || columnName == GroupTable.MEMBERS
|
return columnName == GroupTable.V2_DECRYPTED_GROUP
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
||||||
|
@ -23,8 +22,7 @@ object GV2Transformer : ColumnTransformer {
|
||||||
val group = DecryptedGroup.parseFrom(groupBytes)
|
val group = DecryptedGroup.parseFrom(groupBytes)
|
||||||
group.formatAsHtml()
|
group.formatAsHtml()
|
||||||
} else {
|
} else {
|
||||||
val members = cursor.requireString(GroupTable.MEMBERS)
|
""
|
||||||
members?.split(',')?.chunked(20)?.joinToString("<br>") { it.joinToString(",") } ?: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue