Add new My Story privacy settings.

This commit is contained in:
Cody Henthorne
2022-06-24 10:51:26 -04:00
parent ebc556801e
commit 9bc25132c3
58 changed files with 935 additions and 242 deletions

View File

@@ -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()
}
}

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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")
}
}
}
}

View File

@@ -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
}
}
}