mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Add new My Story privacy settings.
This commit is contained in:
@@ -6,13 +6,19 @@ import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireObject
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -35,6 +41,9 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
@JvmField
|
||||
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||
|
||||
@JvmField
|
||||
val CREATE_INDEXES: Array<String> = arrayOf(MembershipTable.CREATE_INDEX)
|
||||
|
||||
const val RECIPIENT_ID = ListTable.RECIPIENT_ID
|
||||
const val DISTRIBUTION_ID = ListTable.DISTRIBUTION_ID
|
||||
const val LIST_TABLE_NAME = ListTable.TABLE_NAME
|
||||
@@ -55,7 +64,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
ListTable.ID to DistributionListId.MY_STORY_ID,
|
||||
ListTable.NAME to DistributionId.MY_STORY.toString(),
|
||||
ListTable.DISTRIBUTION_ID to DistributionId.MY_STORY.toString(),
|
||||
ListTable.RECIPIENT_ID to recipientId
|
||||
ListTable.RECIPIENT_ID to recipientId,
|
||||
ListTable.PRIVACY_MODE to DistributionListPrivacyMode.ALL.serialize()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -71,8 +81,9 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
const val IS_UNKNOWN = "is_unknown"
|
||||
const val PRIVACY_MODE = "privacy_mode"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$NAME TEXT UNIQUE NOT NULL,
|
||||
@@ -80,11 +91,14 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
$ALLOWS_REPLIES INTEGER DEFAULT 1,
|
||||
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
|
||||
$IS_UNKNOWN INTEGER DEFAULT 0
|
||||
$IS_UNKNOWN INTEGER DEFAULT 0,
|
||||
$PRIVACY_MODE INTEGER DEFAULT ${DistributionListPrivacyMode.ONLY_WITH.serialize()}
|
||||
)
|
||||
"""
|
||||
|
||||
const val IS_NOT_DELETED = "$DELETION_TIMESTAMP == 0"
|
||||
|
||||
val LIST_UI_PROJECTION = arrayOf(ID, NAME, RECIPIENT_ID, ALLOWS_REPLIES, IS_UNKNOWN, PRIVACY_MODE)
|
||||
}
|
||||
|
||||
private object MembershipTable {
|
||||
@@ -93,15 +107,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
const val ID = "_id"
|
||||
const val LIST_ID = "list_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val PRIVACY_MODE = "privacy_mode"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$LIST_ID INTEGER NOT NULL REFERENCES ${ListTable.TABLE_NAME} (${ListTable.ID}) ON DELETE CASCADE,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
UNIQUE($LIST_ID, $RECIPIENT_ID) ON CONFLICT IGNORE
|
||||
$PRIVACY_MODE INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
const val CREATE_INDEX = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON $TABLE_NAME ($LIST_ID, $RECIPIENT_ID, $PRIVACY_MODE)"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,28 +136,13 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
) == 1
|
||||
}
|
||||
|
||||
fun getAllListsForContactSelectionUi(query: String?, includeMyStory: Boolean): List<DistributionListPartialRecord> {
|
||||
return getAllListsForContactSelectionUiCursor(query, includeMyStory)?.use {
|
||||
val results = mutableListOf<DistributionListPartialRecord>()
|
||||
while (it.moveToNext()) {
|
||||
results.add(
|
||||
DistributionListPartialRecord(
|
||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
results
|
||||
} ?: emptyList()
|
||||
fun setPrivacyMode(distributionListId: DistributionListId, privacyMode: DistributionListPrivacyMode) {
|
||||
val values = contentValuesOf(ListTable.PRIVACY_MODE to privacyMode.serialize())
|
||||
writableDatabase.update(ListTable.TABLE_NAME, values, "${ListTable.ID} = ?", SqlUtil.buildArgs(distributionListId))
|
||||
}
|
||||
|
||||
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
|
||||
val db = readableDatabase
|
||||
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||
|
||||
val where = when {
|
||||
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
|
||||
@@ -155,24 +157,32 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
else -> SqlUtil.buildArgs(SqlUtil.buildCaseInsensitiveGlobPattern(query), DistributionListId.MY_STORY_ID)
|
||||
}
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, where, whereArgs, null, null, null)
|
||||
return db.query(ListTable.TABLE_NAME, ListTable.LIST_UI_PROJECTION, where, whereArgs, null, null, null)
|
||||
}
|
||||
|
||||
fun getAllListRecipients(): List<RecipientId> {
|
||||
return readableDatabase
|
||||
.select(ListTable.RECIPIENT_ID)
|
||||
.from(ListTable.TABLE_NAME)
|
||||
.run()
|
||||
.readToList { cursor -> RecipientId.from(cursor.requireLong(ListTable.RECIPIENT_ID)) }
|
||||
}
|
||||
|
||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||
val db = readableDatabase
|
||||
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
|
||||
return db.query(ListTable.TABLE_NAME, ListTable.LIST_UI_PROJECTION, selection, null, null, null, null)?.use { cursor ->
|
||||
val results = mutableListOf<DistributionListPartialRecord>()
|
||||
while (it.moveToNext()) {
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(
|
||||
DistributionListPartialRecord(
|
||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||
id = DistributionListId.from(CursorUtil.requireLong(cursor, ListTable.ID)),
|
||||
name = CursorUtil.requireString(cursor, ListTable.NAME),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, ListTable.RECIPIENT_ID)),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -235,7 +245,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
allowsReplies: Boolean = true,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: ByteArray? = null,
|
||||
isUnknown: Boolean = false
|
||||
isUnknown: Boolean = false,
|
||||
privacyMode: DistributionListPrivacyMode = DistributionListPrivacyMode.ONLY_WITH
|
||||
): DistributionListId? {
|
||||
val db = writableDatabase
|
||||
|
||||
@@ -248,6 +259,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
putNull(ListTable.RECIPIENT_ID)
|
||||
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||
put(ListTable.IS_UNKNOWN, isUnknown)
|
||||
put(ListTable.PRIVACY_MODE, privacyMode.serialize())
|
||||
}
|
||||
|
||||
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
||||
@@ -264,7 +276,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
SqlUtil.buildArgs(id)
|
||||
)
|
||||
|
||||
members.forEach { addMemberToList(DistributionListId.from(id), it) }
|
||||
members.forEach { addMemberToList(DistributionListId.from(id), privacyMode, it) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
@@ -311,15 +323,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
|
||||
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -331,15 +346,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
|
||||
val privacyMode = cursor.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getRawMembers(id),
|
||||
rawMembers = getRawMembers(id, privacyMode),
|
||||
members = emptyList(),
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN),
|
||||
privacyMode = privacyMode
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -358,28 +376,36 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
|
||||
fun getMembers(listId: DistributionListId): List<RecipientId> {
|
||||
if (listId == DistributionListId.MY_STORY) {
|
||||
val blockedMembers = getRawMembers(listId).toSet()
|
||||
lateinit var privacyMode: DistributionListPrivacyMode
|
||||
lateinit var rawMembers: List<RecipientId>
|
||||
|
||||
return SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val result = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
val id = RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID))
|
||||
if (!blockedMembers.contains(id)) {
|
||||
result.add(id)
|
||||
}
|
||||
}
|
||||
result
|
||||
} ?: emptyList()
|
||||
} else {
|
||||
return getRawMembers(listId)
|
||||
readableDatabase.withinTransaction {
|
||||
privacyMode = getPrivacyMode(listId)
|
||||
rawMembers = getRawMembers(listId, privacyMode)
|
||||
}
|
||||
|
||||
return when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> {
|
||||
SignalDatabase.recipients
|
||||
.getSignalContacts(false)!!
|
||||
.readToList { it.requireObject(RecipientDatabase.ID, RecipientId.SERIALIZER) }
|
||||
}
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> {
|
||||
SignalDatabase.recipients
|
||||
.getSignalContacts(false)!!
|
||||
.readToList(
|
||||
predicate = { !rawMembers.contains(it) },
|
||||
mapper = { it.requireObject(RecipientDatabase.ID, RecipientId.SERIALIZER) }
|
||||
)
|
||||
}
|
||||
DistributionListPrivacyMode.ONLY_WITH -> rawMembers
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawMembers(listId: DistributionListId): List<RecipientId> {
|
||||
fun getRawMembers(listId: DistributionListId, privacyMode: DistributionListPrivacyMode): List<RecipientId> {
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, null, "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, null, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, privacyMode.serialize()), null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
members.add(RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID)))
|
||||
}
|
||||
@@ -389,15 +415,35 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
|
||||
fun getMemberCount(listId: DistributionListId): Int {
|
||||
return if (listId == DistributionListId.MY_STORY) {
|
||||
SignalDatabase.recipients.getSignalContacts(false)?.count?.let { it - getRawMemberCount(listId) } ?: 0
|
||||
} else {
|
||||
getRawMemberCount(listId)
|
||||
}
|
||||
return getPrivacyData(listId).memberCount
|
||||
}
|
||||
|
||||
fun getRawMemberCount(listId: DistributionListId): Int {
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, SqlUtil.buildArgs("COUNT(*)"), "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
fun getPrivacyData(listId: DistributionListId): DistributionListPrivacyData {
|
||||
lateinit var privacyMode: DistributionListPrivacyMode
|
||||
var rawMemberCount = 0
|
||||
var totalContactCount = 0
|
||||
|
||||
readableDatabase.withinTransaction {
|
||||
privacyMode = getPrivacyMode(listId)
|
||||
rawMemberCount = getRawMemberCount(listId, privacyMode)
|
||||
totalContactCount = SignalDatabase.recipients.getSignalContactsCount(false)
|
||||
}
|
||||
|
||||
val memberCount = when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> totalContactCount
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> totalContactCount - rawMemberCount
|
||||
DistributionListPrivacyMode.ONLY_WITH -> rawMemberCount
|
||||
}
|
||||
|
||||
return DistributionListPrivacyData(
|
||||
privacyMode = privacyMode,
|
||||
rawMemberCount = rawMemberCount,
|
||||
memberCount = memberCount
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRawMemberCount(listId: DistributionListId, privacyMode: DistributionListPrivacyMode): Int {
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, SqlUtil.buildArgs("COUNT(*)"), "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, privacyMode.serialize()), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
cursor.getInt(0)
|
||||
} else {
|
||||
@@ -406,24 +452,46 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberFromList(listId: DistributionListId, member: RecipientId) {
|
||||
writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(listId, member))
|
||||
private fun getPrivacyMode(listId: DistributionListId): DistributionListPrivacyMode {
|
||||
return readableDatabase
|
||||
.select(ListTable.PRIVACY_MODE)
|
||||
.from(ListTable.TABLE_NAME)
|
||||
.where("${ListTable.ID} = ?", listId.serialize())
|
||||
.run()
|
||||
.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.requireObject(ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
|
||||
} else {
|
||||
DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addMemberToList(listId: DistributionListId, member: RecipientId) {
|
||||
fun removeMemberFromList(listId: DistributionListId, privacyMode: DistributionListPrivacyMode, member: RecipientId) {
|
||||
writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, member, privacyMode.serialize()))
|
||||
}
|
||||
|
||||
fun addMemberToList(listId: DistributionListId, privacyMode: DistributionListPrivacyMode, member: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.LIST_ID, listId.serialize())
|
||||
put(MembershipTable.RECIPIENT_ID, member.serialize())
|
||||
put(MembershipTable.PRIVACY_MODE, privacyMode.serialize())
|
||||
}
|
||||
|
||||
writableDatabase.insert(MembershipTable.TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
fun removeAllMembers(listId: DistributionListId) {
|
||||
writableDatabase
|
||||
.delete(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.LIST_ID} = ?", listId.serialize())
|
||||
.run()
|
||||
}
|
||||
|
||||
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.RECIPIENT_ID, newId.serialize())
|
||||
}
|
||||
|
||||
writableDatabase.update(MembershipTable.TABLE_NAME, values, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(oldId))
|
||||
}
|
||||
|
||||
@@ -487,12 +555,19 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
throw AssertionError("Should never try to insert My Story")
|
||||
}
|
||||
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
insert.isBlockList && insert.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
insert.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
createList(
|
||||
name = insert.name,
|
||||
members = insert.recipients.map(RecipientId::from),
|
||||
distributionId = distributionId,
|
||||
allowsReplies = insert.allowsReplies(),
|
||||
deletionTimestamp = insert.deletedAtTimestamp,
|
||||
privacyMode = privacyMode,
|
||||
storageId = insert.id.raw
|
||||
)
|
||||
}
|
||||
@@ -526,12 +601,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
return
|
||||
}
|
||||
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
update.new.isBlockList && update.new.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
update.new.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val listTableValues = contentValuesOf(
|
||||
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
|
||||
ListTable.NAME to update.new.name,
|
||||
ListTable.IS_UNKNOWN to false
|
||||
ListTable.IS_UNKNOWN to false,
|
||||
ListTable.PRIVACY_MODE to privacyMode.serialize()
|
||||
)
|
||||
|
||||
writableDatabase.update(
|
||||
@@ -541,22 +622,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
SqlUtil.buildArgs(distributionId.toString())
|
||||
)
|
||||
|
||||
val currentlyInDistributionList = getRawMembers(distributionListId).toSet()
|
||||
val currentlyInDistributionList = getRawMembers(distributionListId, privacyMode).toSet()
|
||||
val shouldBeInDistributionList = update.new.recipients.map(RecipientId::from).toSet()
|
||||
val toRemove = currentlyInDistributionList - shouldBeInDistributionList
|
||||
val toAdd = shouldBeInDistributionList - currentlyInDistributionList
|
||||
|
||||
toRemove.forEach {
|
||||
removeMemberFromList(distributionListId, it)
|
||||
removeMemberFromList(distributionListId, privacyMode, it)
|
||||
}
|
||||
|
||||
toAdd.forEach {
|
||||
addMemberToList(distributionListId, it)
|
||||
addMemberToList(distributionListId, privacyMode, it)
|
||||
}
|
||||
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
writableDatabase.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.InvalidKeyException
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
@@ -821,6 +822,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
fun markNeedsSync(recipientIds: Collection<RecipientId>) {
|
||||
writableDatabase
|
||||
.withinTransaction {
|
||||
for (recipientId in recipientIds) {
|
||||
markNeedsSync(recipientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markNeedsSync(recipientId: RecipientId) {
|
||||
rotateStorageId(recipientId)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
|
||||
@@ -2301,6 +2311,14 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
fun getSignalContacts(includeSelf: Boolean): Cursor? {
|
||||
return getSignalContacts(includeSelf, "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE")
|
||||
}
|
||||
|
||||
fun getSignalContactsCount(includeSelf: Boolean): Int {
|
||||
return getSignalContacts(includeSelf)?.count ?: 0
|
||||
}
|
||||
|
||||
fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? {
|
||||
val searchSelection = ContactSearchSelection.Builder()
|
||||
.withRegistered(true)
|
||||
.withGroups(false)
|
||||
@@ -2308,7 +2326,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.build()
|
||||
val selection = searchSelection.where
|
||||
val args = searchSelection.args
|
||||
val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE"
|
||||
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,14 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
private final net.zetetic.database.sqlcipher.SQLiteDatabase wrapped;
|
||||
private final Tracer tracer;
|
||||
|
||||
private static final ThreadLocal<Set<Runnable>> POST_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Set<Runnable>> PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS;
|
||||
private static final ThreadLocal<Set<Runnable>> POST_SUCCESSFUL_TRANSACTION_TASKS;
|
||||
|
||||
static {
|
||||
POST_TRANSACTION_TASKS.set(new LinkedHashSet<>());
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
POST_SUCCESSFUL_TRANSACTION_TASKS = new ThreadLocal<>();
|
||||
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.set(new LinkedHashSet<>());
|
||||
}
|
||||
|
||||
public SQLiteDatabase(net.zetetic.database.sqlcipher.SQLiteDatabase wrapped) {
|
||||
@@ -125,7 +130,7 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
*/
|
||||
public void runPostSuccessfulTransaction(@NonNull Runnable task) {
|
||||
if (wrapped.inTransaction()) {
|
||||
getPostTransactionTasks().add(task);
|
||||
getPendingPostSuccessfulTransactionTasks().add(task);
|
||||
} else {
|
||||
task.run();
|
||||
}
|
||||
@@ -137,18 +142,29 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
*/
|
||||
public void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable task) {
|
||||
if (wrapped.inTransaction()) {
|
||||
getPostTransactionTasks().add(new DedupedRunnable(dedupeKey, task));
|
||||
getPendingPostSuccessfulTransactionTasks().add(new DedupedRunnable(dedupeKey, task));
|
||||
} else {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Set<Runnable> getPostTransactionTasks() {
|
||||
Set<Runnable> tasks = POST_TRANSACTION_TASKS.get();
|
||||
private @NonNull Set<Runnable> getPendingPostSuccessfulTransactionTasks() {
|
||||
Set<Runnable> tasks = PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.get();
|
||||
|
||||
if (tasks == null) {
|
||||
tasks = new LinkedHashSet<>();
|
||||
POST_TRANSACTION_TASKS.set(tasks);
|
||||
PENDING_POST_SUCCESSFUL_TRANSACTION_TASKS.set(tasks);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private @NonNull Set<Runnable> getPostSuccessfulTransactionTasks() {
|
||||
Set<Runnable> tasks = POST_SUCCESSFUL_TRANSACTION_TASKS.get();
|
||||
|
||||
if (tasks == null) {
|
||||
tasks = new LinkedHashSet<>();
|
||||
POST_SUCCESSFUL_TRANSACTION_TASKS.set(tasks);
|
||||
}
|
||||
|
||||
return tasks;
|
||||
@@ -278,16 +294,16 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
|
||||
@Override
|
||||
public void onCommit() {
|
||||
Set<Runnable> tasks = getPostTransactionTasks();
|
||||
for (Runnable r : new HashSet<>(tasks)) {
|
||||
r.run();
|
||||
}
|
||||
Set<Runnable> pendingTasks = getPendingPostSuccessfulTransactionTasks();
|
||||
Set<Runnable> tasks = getPostSuccessfulTransactionTasks();
|
||||
tasks.clear();
|
||||
tasks.addAll(pendingTasks);
|
||||
pendingTasks.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRollback() {
|
||||
getPostTransactionTasks().clear();
|
||||
getPendingPostSuccessfulTransactionTasks().clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -297,6 +313,12 @@ public class SQLiteDatabase implements SupportSQLiteDatabase {
|
||||
public void endTransaction() {
|
||||
trace("endTransaction()", wrapped::endTransaction);
|
||||
traceLockEnd();
|
||||
|
||||
Set<Runnable> tasks = getPostSuccessfulTransactionTasks();
|
||||
for (Runnable r : new HashSet<>(tasks)) {
|
||||
r.run();
|
||||
}
|
||||
tasks.clear();
|
||||
}
|
||||
|
||||
public void setTransactionSuccessful() {
|
||||
|
||||
@@ -131,6 +131,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
|
||||
executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS)
|
||||
db.execSQL(StorySendsDatabase.CREATE_INDEX)
|
||||
executeStatements(db, DistributionListDatabase.CREATE_INDEXES)
|
||||
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||
|
||||
@@ -201,8 +201,9 @@ object SignalDatabaseMigrations {
|
||||
private const val GROUP_STORY_REPLY_CLEANUP = 145
|
||||
private const val REMOTE_MEGAPHONE = 146
|
||||
private const val QUOTE_INDEX = 147
|
||||
private const val MY_STORY_PRIVACY_MODE = 148
|
||||
|
||||
const val DATABASE_VERSION = 147
|
||||
const val DATABASE_VERSION = 148
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2622,6 +2623,41 @@ object SignalDatabaseMigrations {
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
if (oldVersion < MY_STORY_PRIVACY_MODE) {
|
||||
db.execSQL("ALTER TABLE distribution_list ADD COLUMN privacy_mode INTEGER DEFAULT 0")
|
||||
db.execSQL("UPDATE distribution_list SET privacy_mode = 1 WHERE _id = 1")
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE distribution_list_member_tmp (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id),
|
||||
privacy_mode INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO distribution_list_member_tmp
|
||||
SELECT
|
||||
_id,
|
||||
list_id,
|
||||
recipient_id,
|
||||
0
|
||||
FROM distribution_list_member
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL("DROP TABLE distribution_list_member")
|
||||
db.execSQL("ALTER TABLE distribution_list_member_tmp RENAME TO distribution_list_member")
|
||||
|
||||
db.execSQL("UPDATE distribution_list_member SET privacy_mode = 1 WHERE list_id = 1")
|
||||
|
||||
db.execSQL("CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class DistributionListId implements DatabaseId, Parcelable {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public boolean isMyStory() {
|
||||
return equals(MY_STORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(id);
|
||||
|
||||
@@ -7,5 +7,6 @@ data class DistributionListPartialRecord(
|
||||
val name: CharSequence,
|
||||
val recipientId: RecipientId,
|
||||
val allowsReplies: Boolean,
|
||||
val isUnknown: Boolean
|
||||
val isUnknown: Boolean,
|
||||
val privacyMode: DistributionListPrivacyMode
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
/**
|
||||
* Data needed to know how a distribution privacy settings are configured.
|
||||
*/
|
||||
data class DistributionListPrivacyData(
|
||||
val privacyMode: DistributionListPrivacyMode,
|
||||
val rawMemberCount: Int,
|
||||
val memberCount: Int
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* A list can explicit ([ONLY_WITH]) where only members of the list can send or exclusionary ([ALL_EXCEPT]) where
|
||||
* all connections are sent the story except for those members of the list. [ALL] is all of your Signal Connections.
|
||||
*/
|
||||
enum class DistributionListPrivacyMode(private val code: Long) {
|
||||
ONLY_WITH(0),
|
||||
ALL_EXCEPT(1),
|
||||
ALL(2);
|
||||
|
||||
val isBlockList: Boolean
|
||||
get() = this != ONLY_WITH
|
||||
|
||||
fun serialize(): Long {
|
||||
return code
|
||||
}
|
||||
|
||||
companion object Serializer : LongSerializer<DistributionListPrivacyMode> {
|
||||
override fun serialize(data: DistributionListPrivacyMode): Long {
|
||||
return data.serialize()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): DistributionListPrivacyMode {
|
||||
return when (data) {
|
||||
ONLY_WITH.code -> ONLY_WITH
|
||||
ALL_EXCEPT.code -> ALL_EXCEPT
|
||||
ALL.code -> ALL
|
||||
else -> throw AssertionError("Unknown privacy mode: $data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,16 @@ data class DistributionListRecord(
|
||||
val name: String,
|
||||
val distributionId: DistributionId,
|
||||
val allowsReplies: Boolean,
|
||||
val rawMembers: List<RecipientId>,
|
||||
val members: List<RecipientId>,
|
||||
val deletedAtTimestamp: Long,
|
||||
val isUnknown: Boolean
|
||||
)
|
||||
val isUnknown: Boolean,
|
||||
val privacyMode: DistributionListPrivacyMode
|
||||
) {
|
||||
fun getMembersToSync(): List<RecipientId> {
|
||||
return when (privacyMode) {
|
||||
DistributionListPrivacyMode.ALL -> emptyList()
|
||||
else -> rawMembers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user