diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index a3c9f9d2e3..77989df6e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -2,11 +2,16 @@ package org.thoughtcrime.securesms.contacts.paged import android.database.Cursor import org.signal.paging.PagedDataSource +import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchCollection +import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator +import org.thoughtcrime.securesms.contacts.paged.collections.CursorSearchIterator +import org.thoughtcrime.securesms.contacts.paged.collections.StoriesSearchCollection +import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.StorySend +import org.thoughtcrime.securesms.recipients.Recipient import java.util.concurrent.TimeUnit -import kotlin.math.min /** * Manages the querying of contact information based off a configuration. @@ -78,27 +83,30 @@ class ContactSearchPagedDataSource( } private fun getSectionSize(section: ContactSearchConfiguration.Section, query: String?): Int { - when (section) { - is ContactSearchConfiguration.Section.Individuals -> getNonGroupContactsCursor(section, query) - is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupContacts(section, query) - is ContactSearchConfiguration.Section.Recents -> getRecentsCursor(section, query) - is ContactSearchConfiguration.Section.Stories -> getStoriesCursor(query) - }!!.use { cursor -> - val extras: List = when (section) { - is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query) - else -> emptyList() - } - - val collection = createResultsCollection( - section = section, - cursor = cursor, - extraData = extras, - cursorMapper = { error("Unsupported") } - ) - return collection.getSize() + return when (section) { + is ContactSearchConfiguration.Section.Individuals -> getNonGroupSearchIterator(section, query).getCollectionSize(section, query, null) + is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupSearchIterator(section, query).getCollectionSize(section, query, this::canSendToGroup) + is ContactSearchConfiguration.Section.Recents -> getRecentsSearchIterator(section, query).getCollectionSize(section, query, null) + is ContactSearchConfiguration.Section.Stories -> getStoriesSearchIterator(query).getCollectionSize(section, query, null) } } + private fun ContactSearchIterator.getCollectionSize(section: ContactSearchConfiguration.Section, query: String?, recordsPredicate: ((R) -> Boolean)?): Int { + val extras: List = when (section) { + is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query) + else -> emptyList() + } + + val collection = createResultsCollection( + section = section, + records = this, + recordsPredicate = recordsPredicate, + extraData = extras, + recordMapper = { error("Unsupported") } + ) + return collection.getSize() + } + private fun getFilteredGroupStories(section: ContactSearchConfiguration.Section.Stories, query: String?): List { return (contactSearchPagedDataSourceRepository.getGroupStories() + section.groupStories) .filter { contactSearchPagedDataSourceRepository.recipientNameContainsQuery(it.recipient, query) } @@ -113,50 +121,52 @@ class ContactSearchPagedDataSource( } } - private fun getNonGroupContactsCursor(section: ContactSearchConfiguration.Section.Individuals, query: String?): Cursor? { + private fun getNonGroupSearchIterator(section: ContactSearchConfiguration.Section.Individuals, query: String?): ContactSearchIterator { return when (section.transportType) { - ContactSearchConfiguration.TransportType.PUSH -> contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf) - ContactSearchConfiguration.TransportType.SMS -> contactSearchPagedDataSourceRepository.queryNonSignalContacts(query) - ContactSearchConfiguration.TransportType.ALL -> contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf) + ContactSearchConfiguration.TransportType.PUSH -> CursorSearchIterator(contactSearchPagedDataSourceRepository.querySignalContacts(query, section.includeSelf)) + ContactSearchConfiguration.TransportType.SMS -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonSignalContacts(query)) + ContactSearchConfiguration.TransportType.ALL -> CursorSearchIterator(contactSearchPagedDataSourceRepository.queryNonGroupContacts(query, section.includeSelf)) } } - private fun getStoriesCursor(query: String?): Cursor? { - return contactSearchPagedDataSourceRepository.getStories(query) + private fun getStoriesSearchIterator(query: String?): ContactSearchIterator { + return CursorSearchIterator(contactSearchPagedDataSourceRepository.getStories(query)) } - private fun getRecentsCursor(section: ContactSearchConfiguration.Section.Recents, query: String?): Cursor? { + private fun getRecentsSearchIterator(section: ContactSearchConfiguration.Section.Recents, query: String?): ContactSearchIterator { if (!query.isNullOrEmpty()) { throw IllegalArgumentException("Searching Recents is not supported") } - return contactSearchPagedDataSourceRepository.getRecents(section) + return CursorSearchIterator(contactSearchPagedDataSourceRepository.getRecents(section)) } - private fun readContactDataFromCursor( - cursor: Cursor, + private fun readContactData( + records: ContactSearchIterator, + recordsPredicate: ((R) -> Boolean)?, section: ContactSearchConfiguration.Section, startIndex: Int, endIndex: Int, - cursorRowToData: (Cursor) -> ContactSearchData, + recordMapper: (R) -> ContactSearchData, extraData: List = emptyList() ): List { val results = mutableListOf() - val collection = createResultsCollection(section, cursor, extraData, cursorRowToData) + val collection = createResultsCollection(section, records, recordsPredicate, extraData, recordMapper) results.addAll(collection.getSublist(startIndex, endIndex)) return results } private fun getStoriesContactData(section: ContactSearchConfiguration.Section.Stories, query: String?, startIndex: Int, endIndex: Int): List { - return getStoriesCursor(query)?.use { cursor -> - readContactDataFromCursor( - cursor = cursor, + return getStoriesSearchIterator(query).use { records -> + readContactData( + records = records, + null, section = section, startIndex = startIndex, endIndex = endIndex, - cursorRowToData = { + recordMapper = { val recipient = contactSearchPagedDataSourceRepository.getRecipientFromDistributionListCursor(it) val count = contactSearchPagedDataSourceRepository.getDistributionListMembershipCount(recipient) val privacyMode = contactSearchPagedDataSourceRepository.getPrivacyModeFromDistributionListCursor(it) @@ -164,155 +174,76 @@ class ContactSearchPagedDataSource( }, extraData = getFilteredGroupStories(section, query) ) - } ?: emptyList() + } } private fun getRecentsContactData(section: ContactSearchConfiguration.Section.Recents, query: String?, startIndex: Int, endIndex: Int): List { - return getRecentsCursor(section, query)?.use { cursor -> - readContactDataFromCursor( - cursor = cursor, + return getRecentsSearchIterator(section, query).use { records -> + readContactData( + records = records, + recordsPredicate = null, section = section, startIndex = startIndex, endIndex = endIndex, - cursorRowToData = { - ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(cursor)) + recordMapper = { + ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromThreadCursor(it)) } ) - } ?: emptyList() + } } private fun getNonGroupContactsData(section: ContactSearchConfiguration.Section.Individuals, query: String?, startIndex: Int, endIndex: Int): List { - return getNonGroupContactsCursor(section, query)?.use { cursor -> - readContactDataFromCursor( - cursor = cursor, + return getNonGroupSearchIterator(section, query).use { records -> + readContactData( + records = records, + recordsPredicate = null, section = section, startIndex = startIndex, endIndex = endIndex, - cursorRowToData = { - ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(cursor)) + recordMapper = { + ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromRecipientCursor(it)) } ) - } ?: emptyList() + } } private fun getGroupContactsData(section: ContactSearchConfiguration.Section.Groups, query: String?, startIndex: Int, endIndex: Int): List { - return contactSearchPagedDataSourceRepository.getGroupContacts(section, query)?.use { cursor -> - readContactDataFromCursor( - cursor = cursor, + return contactSearchPagedDataSourceRepository.getGroupSearchIterator(section, query).use { records -> + readContactData( + records = records, + recordsPredicate = this::canSendToGroup, section = section, startIndex = startIndex, endIndex = endIndex, - cursorRowToData = { + recordMapper = { if (section.returnAsGroupStories) { - ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupCursor(cursor), 0, DistributionListPrivacyMode.ALL) + ContactSearchData.Story(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it), 0, DistributionListPrivacyMode.ALL) } else { - ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromGroupCursor(cursor)) + ContactSearchData.KnownRecipient(contactSearchPagedDataSourceRepository.getRecipientFromGroupRecord(it)) } } ) - } ?: emptyList() + } } - private fun createResultsCollection( + private fun canSendToGroup(groupRecord: GroupRecord): Boolean { + return if (groupRecord.isAnnouncementGroup) { + groupRecord.isAdmin(Recipient.self()) + } else { + groupRecord.isActive + } + } + + private fun createResultsCollection( section: ContactSearchConfiguration.Section, - cursor: Cursor, + records: ContactSearchIterator, + recordsPredicate: ((R) -> Boolean)?, extraData: List, - cursorMapper: (Cursor) -> ContactSearchData - ): ResultsCollection { + recordMapper: (R) -> ContactSearchData + ): ContactSearchCollection { return when (section) { - is ContactSearchConfiguration.Section.Stories -> StoriesCollection(section, cursor, extraData, cursorMapper, activeStoryCount, StoryComparator(latestStorySends)) - else -> ResultsCollection(section, cursor, extraData, cursorMapper, 0) - } - } - - /** - * We assume that the collection is [cursor contents] + [extraData contents] - */ - private open class ResultsCollection( - val section: ContactSearchConfiguration.Section, - val cursor: Cursor, - val extraData: List, - val cursorMapper: (Cursor) -> ContactSearchData, - val activeContactCount: Int - ) { - - private val contentSize = cursor.count + extraData.count() - - fun getSize(): Int { - val contentsAndExpand = min( - section.expandConfig?.let { - if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded(activeContactCount) + 1) - } ?: Int.MAX_VALUE, - contentSize - ) - - return contentsAndExpand + (if (contentsAndExpand > 0 && section.includeHeader) 1 else 0) - } - - fun getSublist(start: Int, end: Int): List { - val results = mutableListOf() - for (i in start until end) { - results.add(getItemAt(i)) - } - - return results - } - - private fun getItemAt(index: Int): ContactSearchData { - return when { - index == 0 && section.includeHeader -> ContactSearchData.Header(section.sectionKey, section.headerAction) - index == getSize() - 1 && shouldDisplayExpandRow() -> ContactSearchData.Expand(section.sectionKey) - else -> { - val correctedIndex = if (section.includeHeader) index - 1 else index - return getItemAtCorrectedIndex(correctedIndex) - } - } - } - - protected open fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData { - return if (correctedIndex < cursor.count) { - cursor.moveToPosition(correctedIndex) - cursorMapper.invoke(cursor) - } else { - val extraIndex = correctedIndex - cursor.count - extraData[extraIndex] - } - } - - private fun shouldDisplayExpandRow(): Boolean { - val expandConfig = section.expandConfig - return when { - expandConfig == null || expandConfig.isExpanded -> false - else -> contentSize > expandConfig.maxCountWhenNotExpanded(activeContactCount) + 1 - } - } - } - - private class StoriesCollection( - section: ContactSearchConfiguration.Section, - cursor: Cursor, - extraData: List, - cursorMapper: (Cursor) -> ContactSearchData, - activeContactCount: Int, - val storyComparator: StoryComparator - ) : ResultsCollection(section, cursor, extraData, cursorMapper, activeContactCount) { - private val aggregateStoryData: List by lazy { - if (section !is ContactSearchConfiguration.Section.Stories) { - error("Aggregate data creation is only necessary for stories.") - } - - val cursorContacts: List = (0 until cursor.count).map { - cursor.moveToPosition(it) - cursorMapper(cursor) - } - - (cursorContacts + extraData) - .filterIsInstance(ContactSearchData.Story::class.java) - .sortedWith(storyComparator) - } - - override fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData { - return aggregateStoryData[correctedIndex] + is ContactSearchConfiguration.Section.Stories -> StoriesSearchCollection(section, records, extraData, recordMapper, activeStoryCount, StoryComparator(latestStorySends)) + else -> ContactSearchCollection(section, records, recordsPredicate, extraData, recordMapper, 0) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt index cc212fb7d1..e5552ffa54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceRepository.kt @@ -5,8 +5,10 @@ import android.database.Cursor import org.signal.core.util.CursorUtil import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.ContactRepository +import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator import org.thoughtcrime.securesms.database.DistributionListDatabase import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode @@ -42,10 +44,10 @@ open class ContactSearchPagedDataSourceRepository( return contactRepository.queryNonGroupContacts(query ?: "", includeSelf) } - open fun getGroupContacts( + open fun getGroupSearchIterator( section: ContactSearchConfiguration.Section.Groups, query: String? - ): Cursor? { + ): ContactSearchIterator { return SignalDatabase.groups.queryGroups( GroupDatabase.GroupQuery.Builder() .withSearchQuery(query) @@ -54,7 +56,7 @@ open class ContactSearchPagedDataSourceRepository( .withV1Groups(section.includeV1) .withSortOrder(section.sortOrder) .build() - ).cursor + ) } open fun getRecents(section: ContactSearchConfiguration.Section.Recents): Cursor? { @@ -89,8 +91,8 @@ open class ContactSearchPagedDataSourceRepository( return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, ContactRepository.ID_COLUMN))) } - open fun getRecipientFromGroupCursor(cursor: Cursor): Recipient { - return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, GroupDatabase.RECIPIENT_ID))) + open fun getRecipientFromGroupRecord(groupRecord: GroupRecord): Recipient { + return Recipient.resolved(groupRecord.recipientId) } open fun getDistributionListMembershipCount(recipient: Recipient): Int { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchCollection.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchCollection.kt new file mode 100644 index 0000000000..4665d8cefd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchCollection.kt @@ -0,0 +1,125 @@ +package org.thoughtcrime.securesms.contacts.paged.collections + +import androidx.collection.SparseArrayCompat +import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchData +import kotlin.math.min + +/** + * Generic contact search collection. + */ +open class ContactSearchCollection( + private val section: ContactSearchConfiguration.Section, + private val records: ContactSearchIterator, + private val recordPredicate: ((ContactRecord) -> Boolean)? = null, + private val extraData: List, + private val recordMapper: (ContactRecord) -> ContactSearchData, + private val activeContactCount: Int +) { + + private val recordsCount: Int = if (recordPredicate != null) { + records.asSequence().filter(recordPredicate).count() + } else { + records.getCount() + } + + private val contentSize: Int + private val aggregateData: SparseArrayCompat = SparseArrayCompat() + + init { + records.moveToPosition(-1) + contentSize = recordsCount + extraData.count() + } + + fun getSize(): Int { + val contentMaximum = section.expandConfig?.let { + if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded(activeContactCount) + 1) + } ?: Int.MAX_VALUE + + val contentAndExpanded = min(contentMaximum, contentSize) + + return contentAndExpanded + (if (contentAndExpanded > 0 && section.includeHeader) 1 else 0) + } + + fun getSublist(start: Int, end: Int): List { + val results = mutableListOf() + + val startOffset = if (start == 0 && section.includeHeader) { + results.add(ContactSearchData.Header(section.sectionKey, section.headerAction)) + 1 + } else { + 0 + } + + val (expand, endOffset) = if (end == getSize() && shouldDisplayExpandRow()) { + ContactSearchData.Expand(section.sectionKey) to 1 + } else { + null to 0 + } + + fillDataWindow(start, end - start) + for (i in (start + startOffset) until (end - endOffset)) { + val correctedIndex = if (section.includeHeader) i - 1 else i + results.add(getItemAtCorrectedIndex(correctedIndex)) + } + + if (expand != null) { + results.add(expand) + } + + return results + } + + open fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData { + return if (recordPredicate == null) { + records.moveToPosition(correctedIndex - 1) + recordMapper.invoke(records.next()) + } else { + aggregateData.get(correctedIndex)!! + } + } + + open fun fillDataWindow(offset: Int, limit: Int) { + if (recordPredicate == null) { + return + } + + if (isAggregateDataFilled(offset, limit)) { + return + } + + var key = offset + records.moveToPosition(-1) + records.asSequence().filter(recordPredicate).drop(offset).take(limit).forEach { + aggregateData.put(key, recordMapper.invoke(it)) + key++ + } + + if (isAggregateDataFilled(offset, limit)) { + return + } + + extraData.forEach { + aggregateData.put(key, it) + key++ + } + + if (isAggregateDataFilled(offset, limit)) { + return + } + + throw IllegalStateException("Could not fill aggregate data for bounds $offset $limit") + } + + private fun isAggregateDataFilled(startOffset: Int, limit: Int): Boolean { + return (startOffset until (startOffset + limit)).all { aggregateData.containsKey(it) } + } + + private fun shouldDisplayExpandRow(): Boolean { + val expandConfig = section.expandConfig + return when { + expandConfig == null || expandConfig.isExpanded -> false + else -> contentSize > expandConfig.maxCountWhenNotExpanded(activeContactCount) + 1 + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchIterator.kt new file mode 100644 index 0000000000..5a9b88dfc7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/ContactSearchIterator.kt @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.contacts.paged.collections + +import java.io.Closeable + +/** + * Describes the required interface for the ContactSearchPagedDataSource to pull + * and filter the information it needs from the database. + */ +interface ContactSearchIterator : Iterator, Closeable { + fun moveToPosition(n: Int) + fun getCount(): Int +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/CursorSearchIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/CursorSearchIterator.kt new file mode 100644 index 0000000000..28b6a2cfc6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/CursorSearchIterator.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.contacts.paged.collections + +import android.database.Cursor + +class CursorSearchIterator(private val cursor: Cursor?) : ContactSearchIterator { + override fun hasNext(): Boolean = cursor?.let { !it.isLast && !it.isAfterLast } ?: false + + override fun next(): Cursor { + cursor?.moveToNext() + return cursor!! + } + + override fun close() { + cursor?.close() + } + + override fun moveToPosition(n: Int) { + cursor?.moveToPosition(n) + } + + override fun getCount(): Int = cursor?.count ?: 0 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/StoriesSearchCollection.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/StoriesSearchCollection.kt new file mode 100644 index 0000000000..678dcd4e86 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/collections/StoriesSearchCollection.kt @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.contacts.paged.collections + +import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchData + +/** + * Search collection specifically for stories. + */ +class StoriesSearchCollection( + section: ContactSearchConfiguration.Section, + records: ContactSearchIterator, + extraData: List, + recordMapper: (ContactRecord) -> ContactSearchData, + activeContactCount: Int, + private val storyComparator: Comparator +) : ContactSearchCollection(section, records, null, extraData, recordMapper, activeContactCount) { + private val aggregateStoryData: List by lazy { + if (section !is ContactSearchConfiguration.Section.Stories) { + error("Aggregate data creation is only necessary for stories.") + } + + val cursorContacts = records.asSequence().map(recordMapper).toList() + + (cursorContacts + extraData).filterIsInstance(ContactSearchData.Story::class.java).sortedWith(storyComparator) + } + + override fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData { + return aggregateStoryData[correctedIndex] + } + + override fun fillDataWindow(offset: Int, limit: Int) = Unit +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 766cc6dc0d..7193725d6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -25,6 +25,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.EnabledState; import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder; +import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator; import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.BadGroupIdException; @@ -1049,7 +1050,7 @@ public class GroupDatabase extends Database { return result; } - public static class Reader implements Closeable { + public static class Reader implements Closeable, ContactSearchIterator { public final Cursor cursor; @@ -1101,6 +1102,21 @@ public class GroupDatabase extends Database { if (this.cursor != null) this.cursor.close(); } + + @Override + public void moveToPosition(int n) { + cursor.moveToPosition(n); + } + + @Override + public boolean hasNext() { + return !cursor.isLast() && !cursor.isAfterLast(); + } + + @Override + public GroupRecord next() { + return getNext(); + } } public static class GroupRecord { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 32786349a6..777c0c3958 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -252,6 +252,10 @@ public final class PushGroupSendJob extends PushSendJob { if (message.getStoryType().isStory()) { Optional groupRecord = SignalDatabase.groups().getGroup(groupId); + if (groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().isAdmin(Recipient.self())) { + throw new UndeliverableMessageException("Non-admins cannot send stories in announcement groups!"); + } + if (groupRecord.isPresent()) { GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.get().requireV2GroupProperties(); SignalServiceGroupV2 groupContext = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey()) diff --git a/app/src/test/java/org/thoughtcrime/securesms/MockCursor.kt b/app/src/test/java/org/thoughtcrime/securesms/MockCursor.kt index 4cb5e31aaf..7cc7ff705b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/MockCursor.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/MockCursor.kt @@ -28,4 +28,12 @@ abstract class MockCursor : Cursor { return true } + + override fun isLast(): Boolean { + return _position == count - 1 + } + + override fun isAfterLast(): Boolean { + return _position >= count + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt index a384d72faf..6a43b1d571 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSourceTest.kt @@ -24,7 +24,7 @@ class ContactSearchPagedDataSourceTest { @Before fun setUp() { - whenever(repository.getRecipientFromGroupCursor(cursor)).thenReturn(Recipient.UNKNOWN) + whenever(repository.getRecipientFromGroupRecord(any())).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromRecipientCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN) @@ -34,6 +34,8 @@ class ContactSearchPagedDataSourceTest { whenever(cursor.moveToPosition(any())).thenCallRealMethod() whenever(cursor.moveToNext()).thenCallRealMethod() whenever(cursor.position).thenCallRealMethod() + whenever(cursor.isLast).thenCallRealMethod() + whenever(cursor.isAfterLast).thenCallRealMethod() } @Test