mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Reimplement contact search collection to support group access predicate.
This commit is contained in:
committed by
Cody Henthorne
parent
9dd96148d1
commit
1cea615675
@@ -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<ContactSearchData> = 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 <R> ContactSearchIterator<R>.getCollectionSize(section: ContactSearchConfiguration.Section, query: String?, recordsPredicate: ((R) -> Boolean)?): Int {
|
||||
val extras: List<ContactSearchData> = 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<ContactSearchData> {
|
||||
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<Cursor> {
|
||||
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<Cursor> {
|
||||
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<Cursor> {
|
||||
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 <R> readContactData(
|
||||
records: ContactSearchIterator<R>,
|
||||
recordsPredicate: ((R) -> Boolean)?,
|
||||
section: ContactSearchConfiguration.Section,
|
||||
startIndex: Int,
|
||||
endIndex: Int,
|
||||
cursorRowToData: (Cursor) -> ContactSearchData,
|
||||
recordMapper: (R) -> ContactSearchData,
|
||||
extraData: List<ContactSearchData> = emptyList()
|
||||
): List<ContactSearchData> {
|
||||
val results = mutableListOf<ContactSearchData>()
|
||||
|
||||
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<ContactSearchData> {
|
||||
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<ContactSearchData> {
|
||||
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<ContactSearchData> {
|
||||
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<ContactSearchData> {
|
||||
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 <R> createResultsCollection(
|
||||
section: ContactSearchConfiguration.Section,
|
||||
cursor: Cursor,
|
||||
records: ContactSearchIterator<R>,
|
||||
recordsPredicate: ((R) -> Boolean)?,
|
||||
extraData: List<ContactSearchData>,
|
||||
cursorMapper: (Cursor) -> ContactSearchData
|
||||
): ResultsCollection {
|
||||
recordMapper: (R) -> ContactSearchData
|
||||
): ContactSearchCollection<R> {
|
||||
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<ContactSearchData>,
|
||||
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<ContactSearchData> {
|
||||
val results = mutableListOf<ContactSearchData>()
|
||||
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<ContactSearchData>,
|
||||
cursorMapper: (Cursor) -> ContactSearchData,
|
||||
activeContactCount: Int,
|
||||
val storyComparator: StoryComparator
|
||||
) : ResultsCollection(section, cursor, extraData, cursorMapper, activeContactCount) {
|
||||
private val aggregateStoryData: List<ContactSearchData.Story> by lazy {
|
||||
if (section !is ContactSearchConfiguration.Section.Stories) {
|
||||
error("Aggregate data creation is only necessary for stories.")
|
||||
}
|
||||
|
||||
val cursorContacts: List<ContactSearchData> = (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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GroupRecord> {
|
||||
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 {
|
||||
|
||||
@@ -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<ContactRecord>(
|
||||
private val section: ContactSearchConfiguration.Section,
|
||||
private val records: ContactSearchIterator<ContactRecord>,
|
||||
private val recordPredicate: ((ContactRecord) -> Boolean)? = null,
|
||||
private val extraData: List<ContactSearchData>,
|
||||
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<ContactSearchData> = 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<ContactSearchData> {
|
||||
val results = mutableListOf<ContactSearchData>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ContactRecord> : Iterator<ContactRecord>, Closeable {
|
||||
fun moveToPosition(n: Int)
|
||||
fun getCount(): Int
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.contacts.paged.collections
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
class CursorSearchIterator(private val cursor: Cursor?) : ContactSearchIterator<Cursor> {
|
||||
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
|
||||
}
|
||||
@@ -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<ContactRecord>(
|
||||
section: ContactSearchConfiguration.Section,
|
||||
records: ContactSearchIterator<ContactRecord>,
|
||||
extraData: List<ContactSearchData>,
|
||||
recordMapper: (ContactRecord) -> ContactSearchData,
|
||||
activeContactCount: Int,
|
||||
private val storyComparator: Comparator<ContactSearchData.Story>
|
||||
) : ContactSearchCollection<ContactRecord>(section, records, null, extraData, recordMapper, activeContactCount) {
|
||||
private val aggregateStoryData: List<ContactSearchData.Story> 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
|
||||
}
|
||||
@@ -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<GroupRecord> {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -252,6 +252,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
if (message.getStoryType().isStory()) {
|
||||
Optional<GroupDatabase.GroupRecord> 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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user