mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 20:24:32 +01:00
Implement Stories feature behind flag.
Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com> Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
@@ -40,6 +40,7 @@ public class DatabaseObserver {
|
||||
private static final String KEY_MESSAGE_INSERT = "MessageInsert:";
|
||||
private static final String KEY_NOTIFICATION_PROFILES = "NotificationProfiles";
|
||||
private static final String KEY_RECIPIENT = "Recipient";
|
||||
private static final String KEY_STORY_OBSERVER = "Story";
|
||||
|
||||
private final Application application;
|
||||
private final Executor executor;
|
||||
@@ -56,6 +57,7 @@ public class DatabaseObserver {
|
||||
private final Set<MessageObserver> messageUpdateObservers;
|
||||
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
||||
private final Set<Observer> notificationProfileObservers;
|
||||
private final Map<RecipientId, Set<Observer>> storyObservers;
|
||||
|
||||
public DatabaseObserver(Application application) {
|
||||
this.application = application;
|
||||
@@ -72,6 +74,7 @@ public class DatabaseObserver {
|
||||
this.messageUpdateObservers = new HashSet<>();
|
||||
this.messageInsertObservers = new HashMap<>();
|
||||
this.notificationProfileObservers = new HashSet<>();
|
||||
this.storyObservers = new HashMap<>();
|
||||
}
|
||||
|
||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||
@@ -146,6 +149,15 @@ public class DatabaseObserver {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer which will be notified whenever a new Story message is inserted into the database.
|
||||
*/
|
||||
public void registerStoryObserver(@NonNull RecipientId recipientId, @NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
registerMapped(storyObservers, recipientId, listener);
|
||||
});
|
||||
}
|
||||
|
||||
public void unregisterObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
conversationListObservers.remove(listener);
|
||||
@@ -157,6 +169,7 @@ public class DatabaseObserver {
|
||||
stickerPackObservers.remove(listener);
|
||||
attachmentObservers.remove(listener);
|
||||
notificationProfileObservers.remove(listener);
|
||||
unregisterMapped(storyObservers, listener);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,6 +275,12 @@ public class DatabaseObserver {
|
||||
});
|
||||
}
|
||||
|
||||
public void notifyStoryObservers(@NonNull RecipientId recipientId) {
|
||||
runPostSuccessfulTransaction(KEY_STORY_OBSERVER, () -> {
|
||||
notifyMapped(storyObservers, recipientId);
|
||||
});
|
||||
}
|
||||
|
||||
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
|
||||
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
|
||||
executor.execute(runnable);
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.SqlUtil
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Stores distribution lists, which represent different sets of people you may want to share a story with.
|
||||
*/
|
||||
class DistributionListDatabase constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||
|
||||
const val RECIPIENT_ID = ListTable.RECIPIENT_ID
|
||||
|
||||
fun insertInitialDistributionListAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
val recipientId = db.insert(
|
||||
RecipientDatabase.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.DISTRIBUTION_LIST_ID to DistributionListId.MY_STORY_ID,
|
||||
RecipientDatabase.STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||
RecipientDatabase.PROFILE_SHARING to 1
|
||||
)
|
||||
)
|
||||
val listUUID = UUID.randomUUID().toString()
|
||||
db.insert(
|
||||
ListTable.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
ListTable.ID to DistributionListId.MY_STORY_ID,
|
||||
ListTable.NAME to listUUID,
|
||||
ListTable.DISTRIBUTION_ID to listUUID,
|
||||
ListTable.RECIPIENT_ID to recipientId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private object ListTable {
|
||||
const val TABLE_NAME = "distribution_list"
|
||||
|
||||
const val ID = "_id"
|
||||
const val NAME = "name"
|
||||
const val DISTRIBUTION_ID = "distribution_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$NAME TEXT UNIQUE NOT NULL,
|
||||
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID})
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
private object MembershipTable {
|
||||
const val TABLE_NAME = "distribution_list_member"
|
||||
|
||||
const val ID = "_id"
|
||||
const val LIST_ID = "list_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
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
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the name change happened, false otherwise.
|
||||
*/
|
||||
fun setName(distributionListId: DistributionListId, name: String): Boolean {
|
||||
val db = writableDatabase
|
||||
|
||||
return db.updateWithOnConflict(
|
||||
ListTable.TABLE_NAME,
|
||||
contentValuesOf(ListTable.NAME to name),
|
||||
ID_WHERE,
|
||||
SqlUtil.buildArgs(distributionListId),
|
||||
SQLiteDatabase.CONFLICT_IGNORE
|
||||
) == 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),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
results
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
|
||||
val db = readableDatabase
|
||||
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID)
|
||||
|
||||
val where = when {
|
||||
query.isNullOrEmpty() && includeMyStory -> null
|
||||
query.isNullOrEmpty() -> "${ListTable.ID} != ?"
|
||||
includeMyStory -> "${ListTable.NAME} LIKE ? OR ${ListTable.ID} == ?"
|
||||
else -> "${ListTable.NAME} LIKE ? AND ${ListTable.ID} != ?"
|
||||
}
|
||||
|
||||
val whereArgs = when {
|
||||
query.isNullOrEmpty() && includeMyStory -> null
|
||||
query.isNullOrEmpty() -> SqlUtil.buildArgs(DistributionListId.MY_STORY_ID)
|
||||
else -> SqlUtil.buildArgs("%$query%", DistributionListId.MY_STORY_ID)
|
||||
}
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, where, whereArgs, null, null, null)
|
||||
}
|
||||
|
||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||
val db = readableDatabase
|
||||
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID)
|
||||
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID}"
|
||||
|
||||
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.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),
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
results
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict.
|
||||
*/
|
||||
fun createList(name: String, members: List<RecipientId>): DistributionListId? {
|
||||
val db = writableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val values = ContentValues().apply {
|
||||
put(ListTable.NAME, name)
|
||||
put(ListTable.DISTRIBUTION_ID, UUID.randomUUID().toString())
|
||||
putNull(ListTable.RECIPIENT_ID)
|
||||
}
|
||||
|
||||
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
||||
|
||||
if (id < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.from(id))
|
||||
writableDatabase.update(
|
||||
ListTable.TABLE_NAME,
|
||||
ContentValues().apply { put(ListTable.RECIPIENT_ID, recipientId.serialize()) },
|
||||
"${ListTable.ID} = ?",
|
||||
SqlUtil.buildArgs(id)
|
||||
)
|
||||
|
||||
members.forEach { addMemberToList(DistributionListId.from(id), it) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
return DistributionListId.from(id)
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
fun getList(listId: DistributionListId): DistributionListRecord? {
|
||||
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))
|
||||
|
||||
DistributionListRecord(
|
||||
id = id,
|
||||
name = cursor.requireNonNullString(ListTable.NAME),
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
members = getMembers(id)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDistributionId(listId: DistributionListId): DistributionId? {
|
||||
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.DISTRIBUTION_ID), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
DistributionId.from(cursor.requireString(ListTable.DISTRIBUTION_ID))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMembers(listId: DistributionListId): List<RecipientId> {
|
||||
if (listId == DistributionListId.MY_STORY) {
|
||||
val blockedMembers = getRawMembers(listId).toSet()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawMembers(listId: DistributionListId): List<RecipientId> {
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, null, "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
members.add(RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID)))
|
||||
}
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
fun getMemberCount(listId: DistributionListId): Int {
|
||||
return if (listId == DistributionListId.MY_STORY) {
|
||||
SignalDatabase.recipients.getSignalContacts(false)?.count?.let { it - getRawMemberCount(listId) } ?: 0
|
||||
} else {
|
||||
getRawMemberCount(listId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawMemberCount(listId: DistributionListId): Int {
|
||||
readableDatabase.query(MembershipTable.TABLE_NAME, SqlUtil.buildArgs("COUNT(*)"), "${MembershipTable.LIST_ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
cursor.getInt(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberFromList(listId: DistributionListId, member: RecipientId) {
|
||||
writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(listId, member))
|
||||
}
|
||||
|
||||
fun addMemberToList(listId: DistributionListId, member: RecipientId) {
|
||||
val values = ContentValues().apply {
|
||||
put(MembershipTable.LIST_ID, listId.serialize())
|
||||
put(MembershipTable.RECIPIENT_ID, member.serialize())
|
||||
}
|
||||
|
||||
writableDatabase.insert(MembershipTable.TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fun deleteList(distributionListId: DistributionListId) {
|
||||
writableDatabase.delete(ListTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(distributionListId))
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class GroupDatabase extends Database {
|
||||
static final String TABLE_NAME = "groups";
|
||||
private static final String ID = "_id";
|
||||
static final String GROUP_ID = "group_id";
|
||||
static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
private static final String TITLE = "title";
|
||||
static final String MEMBERS = "members";
|
||||
private static final String AVATAR_ID = "avatar_id";
|
||||
@@ -737,7 +737,7 @@ private static final String[] GROUP_PROJECTION = {
|
||||
|
||||
if (removed.size() > 0) {
|
||||
Log.i(TAG, removed.size() + " members were removed from group " + groupId + ". Rotating the DistributionId " + distributionId);
|
||||
SenderKeyUtil.rotateOurKey(context, distributionId);
|
||||
SenderKeyUtil.rotateOurKey(distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,7 +961,7 @@ private static final String[] GROUP_PROJECTION = {
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
public final Cursor cursor;
|
||||
|
||||
public Reader(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
|
||||
@@ -180,6 +180,20 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
|
||||
public abstract void ensureMigration();
|
||||
|
||||
public abstract boolean isStory(long messageId);
|
||||
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
||||
public abstract @NonNull Reader getAllOutgoingStories();
|
||||
public abstract @NonNull Reader getAllStories();
|
||||
public abstract @NonNull List<RecipientId> getAllStoriesRecipientsList();
|
||||
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
|
||||
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
|
||||
public abstract int getNumberOfStoryReplies(long parentStoryId);
|
||||
public abstract boolean hasSelfReplyInStory(long parentStoryId);
|
||||
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
||||
public abstract long getUnreadStoryCount();
|
||||
public abstract @Nullable Long getOldestStorySendTimestamp();
|
||||
public abstract int deleteStoriesOlderThan(long timestamp);
|
||||
|
||||
final @NonNull String getOutgoingTypeClause() {
|
||||
List<String> segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length);
|
||||
for (long outgoingMessageType : Types.OUTGOING_MESSAGE_TYPES) {
|
||||
|
||||
@@ -129,6 +129,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
static final String MESSAGE_RANGES = "ranges";
|
||||
|
||||
public static final String VIEW_ONCE = "reveal_duration";
|
||||
static final String IS_STORY = "is_story";
|
||||
static final String PARENT_STORY_ID = "parent_story_id";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
THREAD_ID + " INTEGER, " +
|
||||
@@ -171,9 +173,11 @@ public class MmsDatabase extends MessageDatabase {
|
||||
MENTIONS_SELF + " INTEGER DEFAULT 0, " +
|
||||
NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
SERVER_GUID + " TEXT DEFAULT NULL, "+
|
||||
SERVER_GUID + " TEXT DEFAULT NULL, " +
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " +
|
||||
MESSAGE_RANGES + " BLOB DEFAULT NULL);";
|
||||
MESSAGE_RANGES + " BLOB DEFAULT NULL, " +
|
||||
IS_STORY + " INTEGER DEFAULT 0, " +
|
||||
PARENT_STORY_ID + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");",
|
||||
@@ -181,7 +185,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
"CREATE INDEX IF NOT EXISTS mms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ", " + RECIPIENT_ID + ", " + THREAD_ID + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");"
|
||||
"CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mms_is_story_index ON " + TABLE_NAME + " (" + IS_STORY + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON " + TABLE_NAME + " (" + PARENT_STORY_ID + ");"
|
||||
};
|
||||
|
||||
private static final String[] MMS_PROJECTION = new String[] {
|
||||
@@ -197,6 +203,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP, MESSAGE_RANGES,
|
||||
IS_STORY, PARENT_STORY_ID,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
@@ -229,6 +236,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
|
||||
};
|
||||
|
||||
private static final String IS_STORY_CLAUSE = IS_STORY + " = ? AND " + REMOTE_DELETED + " = ?";
|
||||
|
||||
private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?";
|
||||
|
||||
private static final String OUTGOING_INSECURE_MESSAGES_CLAUSE = "(" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + MESSAGE_BOX + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
@@ -521,6 +530,205 @@ public class MmsDatabase extends MessageDatabase {
|
||||
databaseHelper.getSignalWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStory(long messageId) {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
String[] projection = new String[]{"1"};
|
||||
String where = IS_STORY_CLAUSE + " AND " + ID + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0, messageId);
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
Long threadId = null;
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
threadId = SignalDatabase.threads().getThreadIdFor(recipientId);
|
||||
}
|
||||
|
||||
String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")";
|
||||
|
||||
final String[] whereArgs;
|
||||
if (threadId == null) {
|
||||
where += " AND " + RECIPIENT_ID + " = ?";
|
||||
whereArgs = SqlUtil.buildArgs(1, 0, recipientId);
|
||||
} else {
|
||||
where += " AND " + THREAD_ID_WHERE;
|
||||
whereArgs = SqlUtil.buildArgs(1, 0, threadId);
|
||||
}
|
||||
|
||||
return new Reader(rawQuery(where, whereArgs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllOutgoingStories() {
|
||||
String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")";
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0);
|
||||
|
||||
return new Reader(rawQuery(where, whereArgs, true, -1L));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllStories() {
|
||||
String where = IS_STORY_CLAUSE;
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0);
|
||||
Cursor cursor = rawQuery(where, whereArgs, true, -1L);
|
||||
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
||||
String where = IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE;
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0, threadId);
|
||||
Cursor cursor = rawQuery(where, whereArgs, true, -1L);
|
||||
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
String[] projection = new String[]{ID, RECIPIENT_ID};
|
||||
String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0, sentTimestamp);
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
RecipientId rowRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)));
|
||||
|
||||
if (Recipient.self().getId().equals(authorId) || rowRecipientId.equals(authorId)) {
|
||||
return new MessageId(CursorUtil.requireLong(cursor, ID), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new NoSuchMessageException("No story sent at " + sentTimestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<RecipientId> getAllStoriesRecipientsList() {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String query = "SELECT " +
|
||||
"DISTINCT " + ThreadDatabase.RECIPIENT_ID + " " +
|
||||
"FROM " + TABLE_NAME + " JOIN " + ThreadDatabase.TABLE_NAME + " " +
|
||||
"ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
||||
"WHERE " + IS_STORY_CLAUSE + " " +
|
||||
"ORDER BY " + ThreadDatabase.RECIPIENT_ID + " DESC";
|
||||
String[] args = SqlUtil.buildArgs(1, 0);
|
||||
List<RecipientId> recipientIds;
|
||||
|
||||
try (Cursor cursor = db.rawQuery(query, args)) {
|
||||
if (cursor != null) {
|
||||
recipientIds = new ArrayList<>(cursor.getCount());
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
recipientIds.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)));
|
||||
}
|
||||
|
||||
return recipientIds;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Cursor getStoryReplies(long parentStoryId) {
|
||||
String where = PARENT_STORY_ID + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(parentStoryId);
|
||||
|
||||
return rawQuery(where, whereArgs, true, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUnreadStoryCount() {
|
||||
String[] columns = new String[]{"COUNT(*)"};
|
||||
String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ_RECEIPT_COUNT + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0, 0);
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) {
|
||||
return cursor != null && cursor.moveToFirst() ? cursor.getInt(0) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfStoryReplies(long parentStoryId) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String[] columns = new String[]{"COUNT(*)"};
|
||||
String where = PARENT_STORY_ID + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(parentStoryId);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) {
|
||||
return cursor != null && cursor.moveToNext() ? cursor.getInt(0) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSelfReplyInStory(long parentStoryId) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String[] columns = new String[]{"COUNT(*)"};
|
||||
String where = PARENT_STORY_ID + " = ? AND " + RECIPIENT_ID + " = ?";
|
||||
String[] whereArgs = SqlUtil.buildArgs(parentStoryId, Recipient.self().getId());
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) {
|
||||
return cursor != null && cursor.moveToNext() && cursor.getInt(0) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Long getOldestStorySendTimestamp() {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String[] columns = new String[]{DATE_SENT};
|
||||
String where = IS_STORY_CLAUSE;
|
||||
String[] whereArgs = SqlUtil.buildArgs(1, 0);
|
||||
String orderBy = DATE_SENT + " ASC";
|
||||
String limit = "1";
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, orderBy, limit)) {
|
||||
return cursor != null && cursor.moveToNext() ? cursor.getLong(0) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ?";
|
||||
String[] sharedArgs = SqlUtil.buildArgs(1, 0, timestamp);
|
||||
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + PARENT_STORY_ID + " IN (" +
|
||||
"SELECT " + ID + " " +
|
||||
"FROM " + TABLE_NAME + " " +
|
||||
"WHERE " + storiesBeforeTimestampWhere +
|
||||
")";
|
||||
|
||||
db.rawQuery(deleteStoryRepliesQuery, sharedArgs);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{RECIPIENT_ID}, storiesBeforeTimestampWhere, sharedArgs, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)));
|
||||
ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
int deletedStories = db.delete(TABLE_NAME, storiesBeforeTimestampWhere, sharedArgs);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
return deletedStories;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupQuitMessage(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
@@ -563,7 +771,10 @@ public class MmsDatabase extends MessageDatabase {
|
||||
public int getMessageCountForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, COUNT, THREAD_ID_WHERE, SqlUtil.buildArgs(threadId), null, null, null)) {
|
||||
String query = THREAD_ID + " = ? AND " + IS_STORY + " = ? AND " + PARENT_STORY_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(threadId, 0, 0);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
@@ -576,11 +787,10 @@ public class MmsDatabase extends MessageDatabase {
|
||||
public int getMessageCountForThread(long threadId, long beforeTime) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
String[] cols = new String[] {"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ?";
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(beforeTime)};
|
||||
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ? AND " + IS_STORY + " = ? AND " + PARENT_STORY_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(threadId, beforeTime, 0, 0);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
@@ -1166,6 +1376,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
int distributionType = SignalDatabase.threads().getDistributionType(threadId);
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
|
||||
boolean isStory = CursorUtil.requireBoolean(cursor, IS_STORY);
|
||||
MessageId parentStoryId = MessageId.fromNullable(CursorUtil.requireLong(cursor, PARENT_STORY_ID), true);
|
||||
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
@@ -1214,7 +1426,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions, networkFailures, mismatches);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, isStory, parentStoryId, quote, contacts, previews, mentions, networkFailures, mismatches);
|
||||
|
||||
if (Types.isSecureType(outboxType)) {
|
||||
return new OutgoingSecureMediaMessage(message);
|
||||
@@ -1335,6 +1547,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
contentValues.put(SUBSCRIPTION_ID, retrieved.getSubscriptionId());
|
||||
contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
|
||||
contentValues.put(VIEW_ONCE, retrieved.isViewOnce() ? 1 : 0);
|
||||
contentValues.put(IS_STORY, retrieved.isStory() ? 1 : 0);
|
||||
contentValues.put(PARENT_STORY_ID, retrieved.getParentStoryId() != null ? retrieved.getParentStoryId().getId() : 0);
|
||||
contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0);
|
||||
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified());
|
||||
contentValues.put(SERVER_GUID, retrieved.getServerGuid());
|
||||
@@ -1366,7 +1580,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), retrieved.getMessageRanges(), contentValues, null, true);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
if (!Types.isExpirationTimerUpdate(mailbox) && !retrieved.isStory() && retrieved.getParentStoryId() == null) {
|
||||
SignalDatabase.threads().incrementUnread(threadId, 1);
|
||||
SignalDatabase.threads().update(threadId, true);
|
||||
}
|
||||
@@ -1528,6 +1742,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
contentValues.put(RECIPIENT_ID, message.getRecipient().getId().serialize());
|
||||
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum());
|
||||
contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1));
|
||||
contentValues.put(IS_STORY, message.isStory() ? 1 : 0);
|
||||
contentValues.put(PARENT_STORY_ID, message.getParentStoryId() != null ? message.getParentStoryId().getId() : 0);
|
||||
|
||||
if (message.getRecipient().isSelf() && hasAudioAttachment(message.getAttachments())) {
|
||||
contentValues.put(VIEWED_RECEIPT_COUNT, 1L);
|
||||
@@ -1581,7 +1797,12 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
SignalDatabase.threads().updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId);
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true));
|
||||
if (!message.isStory()) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true));
|
||||
} else {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId());
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
|
||||
TrimThreadJob.enqueueAsync(threadId);
|
||||
|
||||
@@ -28,8 +28,8 @@ import com.annimon.stream.Stream;
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MessageUpdate;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
|
||||
@@ -110,13 +110,15 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
MmsDatabase.MESSAGE_RANGES,
|
||||
MmsDatabase.IS_STORY,
|
||||
MmsDatabase.PARENT_STORY_ID};
|
||||
|
||||
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ", " + SmsDatabase.Types.BOOST_REQUEST_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"UNION ALL " +
|
||||
"SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " = 0 " +
|
||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
||||
"LIMIT 1";
|
||||
|
||||
@@ -200,7 +202,7 @@ public class MmsSmsDatabase extends Database {
|
||||
public Cursor getConversation(long threadId, long offset, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " = 0";
|
||||
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
|
||||
String query = buildQuery(PROJECTION, selection, order, limitStr, false);
|
||||
|
||||
@@ -687,15 +689,29 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " AS " + MmsSmsColumns.ID,
|
||||
"'MMS::' || " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " || '::' || " + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID,
|
||||
attachmentJsonJoin + " AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
|
||||
SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
|
||||
SmsDatabase.TYPE, SmsDatabase.RECIPIENT_ID, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
|
||||
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
|
||||
SmsDatabase.BODY,
|
||||
MmsSmsColumns.READ,
|
||||
MmsSmsColumns.THREAD_ID,
|
||||
SmsDatabase.TYPE,
|
||||
SmsDatabase.RECIPIENT_ID,
|
||||
SmsDatabase.ADDRESS_DEVICE_ID,
|
||||
SmsDatabase.SUBJECT,
|
||||
MmsDatabase.MESSAGE_TYPE,
|
||||
MmsDatabase.MESSAGE_BOX,
|
||||
SmsDatabase.STATUS,
|
||||
MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION,
|
||||
MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE,
|
||||
MmsDatabase.EXPIRY,
|
||||
MmsDatabase.STATUS,
|
||||
MmsDatabase.UNIDENTIFIED,
|
||||
MmsSmsColumns.DELIVERY_RECEIPT_COUNT, MmsSmsColumns.READ_RECEIPT_COUNT,
|
||||
MmsSmsColumns.DELIVERY_RECEIPT_COUNT,
|
||||
MmsSmsColumns.READ_RECEIPT_COUNT,
|
||||
MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
|
||||
MmsSmsColumns.SUBSCRIPTION_ID,
|
||||
MmsSmsColumns.EXPIRES_IN,
|
||||
MmsSmsColumns.EXPIRE_STARTED,
|
||||
MmsSmsColumns.NOTIFIED,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT,
|
||||
MmsDatabase.QUOTE_ID,
|
||||
@@ -715,7 +731,9 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
MmsDatabase.MESSAGE_RANGES,
|
||||
MmsDatabase.IS_STORY,
|
||||
MmsDatabase.PARENT_STORY_ID};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@@ -749,7 +767,9 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
MmsDatabase.MESSAGE_RANGES,
|
||||
"0 AS " + MmsDatabase.IS_STORY,
|
||||
"0 AS " + MmsDatabase.PARENT_STORY_ID };
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@@ -812,6 +832,8 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP);
|
||||
mmsColumnsPresent.add(MmsDatabase.MESSAGE_RANGES);
|
||||
mmsColumnsPresent.add(MmsDatabase.IS_STORY);
|
||||
mmsColumnsPresent.add(MmsDatabase.PARENT_STORY_ID);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
@@ -836,9 +858,11 @@ public class MmsSmsDatabase extends Database {
|
||||
smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
|
||||
smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
smsColumnsPresent.add(MmsSmsColumns.REMOTE_DELETED);
|
||||
smsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP);
|
||||
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP);
|
||||
smsColumnsPresent.add("0 AS " + MmsDatabase.IS_STORY);
|
||||
smsColumnsPresent.add("0 AS " + MmsDatabase.PARENT_STORY_ID);
|
||||
|
||||
String mmsGroupBy = includeAttachments ? MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID : null;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
|
||||
val reactions: MutableList<ReactionRecord> = mutableListOf()
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
|
||||
readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
reactions += readReaction(cursor)
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
val args: List<Array<String>> = messageIds.map { SqlUtil.buildArgs(it.id, if (it.mms) 1 else 0) }
|
||||
|
||||
for (query: SqlUtil.Query in SqlUtil.buildCustomCollectionQuery("$MESSAGE_ID = ? AND $IS_MMS = ?", args)) {
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||
readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val reaction: ReactionRecord = readReaction(cursor)
|
||||
val messageId = MessageId(
|
||||
@@ -115,9 +115,8 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
}
|
||||
|
||||
fun addReaction(messageId: MessageId, reaction: ReactionRecord) {
|
||||
val db: SQLiteDatabase = databaseHelper.signalWritableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
val values = ContentValues().apply {
|
||||
put(MESSAGE_ID, messageId.id)
|
||||
@@ -128,41 +127,40 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
put(DATE_RECEIVED, reaction.dateReceived)
|
||||
}
|
||||
|
||||
db.insert(TABLE_NAME, null, values)
|
||||
writableDatabase.insert(TABLE_NAME, null, values)
|
||||
|
||||
if (messageId.mms) {
|
||||
SignalDatabase.mms.updateReactionsUnread(db, messageId.id, hasReactions(messageId), false)
|
||||
SignalDatabase.mms.updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), false)
|
||||
} else {
|
||||
SignalDatabase.sms.updateReactionsUnread(db, messageId.id, hasReactions(messageId), false)
|
||||
SignalDatabase.sms.updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), false)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
writableDatabase.endTransaction()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId)
|
||||
}
|
||||
|
||||
fun deleteReaction(messageId: MessageId, recipientId: RecipientId) {
|
||||
val db: SQLiteDatabase = databaseHelper.signalWritableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, recipientId)
|
||||
|
||||
db.delete(TABLE_NAME, query, args)
|
||||
writableDatabase.delete(TABLE_NAME, query, args)
|
||||
|
||||
if (messageId.mms) {
|
||||
SignalDatabase.mms.updateReactionsUnread(db, messageId.id, hasReactions(messageId), true)
|
||||
SignalDatabase.mms.updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), true)
|
||||
} else {
|
||||
SignalDatabase.sms.updateReactionsUnread(db, messageId.id, hasReactions(messageId), true)
|
||||
SignalDatabase.sms.updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), true)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
writableDatabase.endTransaction()
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId)
|
||||
@@ -176,7 +174,7 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ? AND $EMOJI = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, reaction.author, reaction.emoji)
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
readableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
}
|
||||
}
|
||||
@@ -185,7 +183,7 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
val query = "$MESSAGE_ID = ? AND $IS_MMS = ?"
|
||||
val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0)
|
||||
|
||||
databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
readableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
}
|
||||
}
|
||||
@@ -197,7 +195,7 @@ class ReactionDatabase(context: Context, databaseHelper: SignalDatabase) : Datab
|
||||
put(AUTHOR_ID, newAuthorId.serialize())
|
||||
}
|
||||
|
||||
databaseHelper.signalWritableDatabase.update(TABLE_NAME, values, query, args)
|
||||
readableDatabase.update(TABLE_NAME, values, query, args)
|
||||
}
|
||||
|
||||
fun deleteAbandonedReactions() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import net.zetetic.database.sqlcipher.SQLiteConstraintException
|
||||
@@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.LegacyGroupInsertException
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.MissedGroupMigrationInsertException
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.distributionLists
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
||||
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notification
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
@@ -88,19 +91,11 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.lang.AssertionError
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.StringBuilder
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import java.util.Collections
|
||||
import java.util.HashMap
|
||||
import java.util.HashSet
|
||||
import java.util.LinkedHashSet
|
||||
import java.util.LinkedList
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.Throws
|
||||
import kotlin.math.max
|
||||
|
||||
open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
|
||||
@@ -117,6 +112,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
const val PHONE = "phone"
|
||||
const val EMAIL = "email"
|
||||
const val GROUP_ID = "group_id"
|
||||
const val DISTRIBUTION_LIST_ID = "distribution_list_id"
|
||||
const val GROUP_TYPE = "group_type"
|
||||
private const val BLOCKED = "blocked"
|
||||
private const val MESSAGE_RINGTONE = "message_ringtone"
|
||||
@@ -141,12 +137,12 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
private const val PROFILE_KEY = "profile_key"
|
||||
private const val PROFILE_KEY_CREDENTIAL = "profile_key_credential"
|
||||
private const val SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"
|
||||
private const val PROFILE_SHARING = "profile_sharing"
|
||||
const val PROFILE_SHARING = "profile_sharing"
|
||||
private const val LAST_PROFILE_FETCH = "last_profile_fetch"
|
||||
private const val UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode"
|
||||
const val FORCE_SMS_SELECTION = "force_sms_selection"
|
||||
private const val CAPABILITIES = "capabilities"
|
||||
private const val STORAGE_SERVICE_ID = "storage_service_key"
|
||||
const val STORAGE_SERVICE_ID = "storage_service_key"
|
||||
private const val PROFILE_GIVEN_NAME = "signal_profile_name"
|
||||
private const val PROFILE_FAMILY_NAME = "profile_family_name"
|
||||
private const val PROFILE_JOINED_NAME = "profile_joined_name"
|
||||
@@ -222,7 +218,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
$CHAT_COLORS BLOB DEFAULT NULL,
|
||||
$CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0,
|
||||
$BADGES BLOB DEFAULT NULL,
|
||||
$PNI_COLUMN TEXT DEFAULT NULL
|
||||
$PNI_COLUMN TEXT DEFAULT NULL,
|
||||
$DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
@@ -280,7 +277,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
GROUPS_IN_COMMON,
|
||||
CHAT_COLORS,
|
||||
CUSTOM_CHAT_COLORS_ID,
|
||||
BADGES
|
||||
BADGES,
|
||||
DISTRIBUTION_LIST_ID
|
||||
)
|
||||
|
||||
private val ID_PROJECTION = arrayOf(ID)
|
||||
@@ -578,6 +576,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
return getOrInsertByColumn(EMAIL, email).recipientId
|
||||
}
|
||||
|
||||
fun getOrInsertFromDistributionListId(distributionListId: DistributionListId): RecipientId {
|
||||
return getOrInsertByColumn(
|
||||
DISTRIBUTION_LIST_ID,
|
||||
distributionListId.serialize(),
|
||||
ContentValues().apply {
|
||||
put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
|
||||
put(PROFILE_SHARING, 1)
|
||||
}
|
||||
).recipientId
|
||||
}
|
||||
|
||||
fun getOrInsertFromGroupId(groupId: GroupId): RecipientId {
|
||||
var existing = getByGroupId(groupId)
|
||||
|
||||
@@ -783,7 +793,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
val values = getValuesForStorageContact(insert, true)
|
||||
val id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE)
|
||||
|
||||
val recipientId: RecipientId?
|
||||
val recipientId: RecipientId
|
||||
if (id < 0) {
|
||||
Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.")
|
||||
recipientId = getAndPossiblyMerge(if (insert.address.hasValidServiceId()) insert.address.serviceId else null, insert.address.number.orNull(), true)
|
||||
@@ -795,13 +805,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
if (insert.identityKey.isPresent && insert.address.hasValidServiceId()) {
|
||||
try {
|
||||
val identityKey = IdentityKey(insert.identityKey.get(), 0)
|
||||
identities.updateIdentityAfterSync(insert.address.identifier, recipientId!!, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState))
|
||||
identities.updateIdentityAfterSync(insert.address.identifier, recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState))
|
||||
} catch (e: InvalidKeyException) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e)
|
||||
}
|
||||
}
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId!!, insert)
|
||||
updateExtras(recipientId) {
|
||||
it.setHideStory(insert.shouldHideStory())
|
||||
}
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, insert)
|
||||
}
|
||||
|
||||
fun applyStorageSyncContactUpdate(update: StorageRecordUpdate<SignalContactRecord>) {
|
||||
@@ -850,6 +864,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
Log.w(TAG, "Failed to process identity key during update! Skipping.", e)
|
||||
}
|
||||
|
||||
updateExtras(recipientId) {
|
||||
it.setHideStory(update.new.shouldHideStory())
|
||||
}
|
||||
|
||||
threads.applyStorageSyncUpdate(recipientId, update.new)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
|
||||
}
|
||||
@@ -891,6 +909,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.build()
|
||||
)
|
||||
|
||||
updateExtras(recipient.id) {
|
||||
it.setHideStory(insert.shouldHideStory())
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling request for latest group info for $groupId")
|
||||
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(groupId))
|
||||
threads.applyStorageSyncUpdate(recipient.id, insert)
|
||||
@@ -908,6 +930,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
val masterKey = update.old.masterKeyOrThrow
|
||||
val recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey))
|
||||
|
||||
updateExtras(recipient.id) {
|
||||
it.setHideStory(update.new.shouldHideStory())
|
||||
}
|
||||
|
||||
threads.applyStorageSyncUpdate(recipient.id, update.new)
|
||||
recipient.live().refresh()
|
||||
}
|
||||
@@ -1015,7 +1041,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All storage IDs for ContactRecords, excluding the ones that need to be deleted.
|
||||
* @return All storage IDs for synced records, excluding the ones that need to be deleted.
|
||||
*/
|
||||
fun getContactStorageSyncIdsMap(): Map<RecipientId, StorageId> {
|
||||
val query = """
|
||||
@@ -1374,6 +1400,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong())
|
||||
|
||||
val values = ContentValues(1).apply {
|
||||
put(CAPABILITIES, value)
|
||||
@@ -1846,6 +1873,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
fun setHideStory(id: RecipientId, hideStory: Boolean) {
|
||||
updateExtras(id) { it.setHideStory(hideStory) }
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun clearUsernameIfExists(username: String) {
|
||||
val existingUsername = getByUsername(username)
|
||||
if (existingUsername.isPresent) {
|
||||
@@ -2444,8 +2476,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
|
||||
}
|
||||
|
||||
val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?) OR $REGISTERED = ?)"
|
||||
val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, RegisteredState.REGISTERED.id)
|
||||
val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?, ?) OR $REGISTERED = ?)"
|
||||
val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, GroupType.DISTRIBUTION_LIST, RegisteredState.REGISTERED.id)
|
||||
writableDatabase.update(TABLE_NAME, values, query, args)
|
||||
}
|
||||
|
||||
@@ -2512,7 +2544,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrInsertByColumn(column: String, value: String): GetOrInsertResult {
|
||||
private fun getOrInsertByColumn(column: String, value: String, contentValues: ContentValues = contentValuesOf(column to value)): GetOrInsertResult {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
throw AssertionError("$column cannot be empty.")
|
||||
}
|
||||
@@ -2522,12 +2554,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
if (existing.isPresent) {
|
||||
return GetOrInsertResult(existing.get(), false)
|
||||
} else {
|
||||
val values = ContentValues().apply {
|
||||
put(column, value)
|
||||
put(AVATAR_COLOR, AvatarColor.random().serialize())
|
||||
}
|
||||
|
||||
val id = writableDatabase.insert(TABLE_NAME, null, values)
|
||||
val id = writableDatabase.insert(TABLE_NAME, null, contentValues)
|
||||
if (id < 0) {
|
||||
existing = getByColumn(column, value)
|
||||
if (existing.isPresent) {
|
||||
@@ -2650,6 +2677,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
// Notification Profiles
|
||||
notificationProfiles.remapRecipient(byE164, byAci)
|
||||
|
||||
// DistributionLists
|
||||
distributionLists.remapRecipient(byE164, byAci)
|
||||
|
||||
// Recipient
|
||||
Log.w(TAG, "Deleting recipient $byE164", true)
|
||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))
|
||||
@@ -2853,6 +2883,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
e164 = cursor.requireString(PHONE),
|
||||
email = cursor.requireString(EMAIL),
|
||||
groupId = GroupId.parseNullableOrThrow(cursor.requireString(GROUP_ID)),
|
||||
distributionListId = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID)),
|
||||
groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE)),
|
||||
isBlocked = cursor.requireBoolean(BLOCKED),
|
||||
muteUntil = cursor.requireLong(MUTE_UNTIL),
|
||||
@@ -2884,6 +2915,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()),
|
||||
announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()),
|
||||
changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()),
|
||||
storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()),
|
||||
insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)),
|
||||
storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)),
|
||||
mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)),
|
||||
@@ -3270,6 +3302,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
const val SENDER_KEY = 2
|
||||
const val ANNOUNCEMENT_GROUPS = 3
|
||||
const val CHANGE_NUMBER = 4
|
||||
const val STORIES = 5
|
||||
}
|
||||
|
||||
enum class VibrateState(val id: Int) {
|
||||
@@ -3321,7 +3354,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
enum class GroupType(val id: Int) {
|
||||
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3);
|
||||
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3), DISTRIBUTION_LIST(4);
|
||||
|
||||
companion object {
|
||||
fun fromId(id: Int): GroupType {
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.SqlUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.io.File
|
||||
import java.lang.UnsupportedOperationException
|
||||
|
||||
open class SignalDatabase(private val context: Application, databaseSecret: DatabaseSecret, attachmentSecret: AttachmentSecret) :
|
||||
SQLiteOpenHelper(
|
||||
@@ -72,6 +71,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val reactionDatabase: ReactionDatabase = ReactionDatabase(context, this)
|
||||
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
|
||||
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
|
||||
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.enableWriteAheadLogging()
|
||||
@@ -109,6 +109,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
||||
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
|
||||
executeStatements(db, DistributionListDatabase.CREATE_TABLE)
|
||||
|
||||
executeStatements(db, RecipientDatabase.CREATE_INDEXS)
|
||||
executeStatements(db, SmsDatabase.CREATE_INDEXS)
|
||||
@@ -130,6 +131,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
|
||||
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
|
||||
|
||||
DistributionListDatabase.insertInitialDistributionListAtCreationTime(db)
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
val legacyHelper = ClassicOpenHelper(context)
|
||||
val legacyDb = legacyHelper.writableDatabase
|
||||
@@ -329,6 +332,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val contacts: ContactsDatabase
|
||||
get() = instance!!.contactsDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("distributionLists")
|
||||
val distributionLists: DistributionListDatabase
|
||||
get() = instance!!.distributionListDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("drafts")
|
||||
val drafts: DraftDatabase
|
||||
@@ -389,6 +397,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val mmsSms: MmsSmsDatabase
|
||||
get() = instance!!.mmsSmsDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("notificationProfiles")
|
||||
val notificationProfiles: NotificationProfileDatabase
|
||||
get() = instance!!.notificationProfileDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("payments")
|
||||
val payments: PaymentDatabase
|
||||
@@ -465,11 +478,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val unknownStorageIds: UnknownStorageIdDatabase
|
||||
get() = instance!!.storageIdDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("notificationProfiles")
|
||||
val notificationProfiles: NotificationProfileDatabase
|
||||
get() = instance!!.notificationProfileDatabase
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("donationReceipts")
|
||||
val donationReceipts: DonationReceiptDatabase
|
||||
|
||||
@@ -1371,6 +1371,71 @@ public class SmsDatabase extends MessageDatabase {
|
||||
databaseHelper.getSignalWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStory(long messageId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllOutgoingStories() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllStories() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<RecipientId> getAllStoriesRecipientsList() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNumberOfStoryReplies(long parentStoryId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSelfReplyInStory(long parentStoryId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Cursor getStoryReplies(long parentStoryId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUnreadStoryCount() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Long getOldestStorySendTimestamp() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
return getSmsMessage(messageId);
|
||||
|
||||
@@ -495,6 +495,19 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public long getUnreadThreadCount() {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String[] projection = SqlUtil.buildArgs("COUNT(*)");
|
||||
String where = READ + " != 1";
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, where, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void incrementUnread(long threadId, int amount) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
@@ -1607,6 +1620,7 @@ public class ThreadDatabase extends Database {
|
||||
recipientSettings,
|
||||
null,
|
||||
false);
|
||||
|
||||
recipient = new Recipient(recipientId, details, false);
|
||||
} else {
|
||||
recipient = Recipient.live(recipientId).get();
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.preference.PreferenceManager
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
|
||||
import org.thoughtcrime.securesms.database.requireString
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -50,6 +52,7 @@ import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -188,8 +191,9 @@ object SignalDatabaseMigrations {
|
||||
private const val REACTION_TRIGGER_FIX = 129
|
||||
private const val PNI_STORES = 130
|
||||
private const val DONATION_RECEIPTS = 131
|
||||
private const val STORIES = 132
|
||||
|
||||
const val DATABASE_VERSION = 131
|
||||
const val DATABASE_VERSION = 132
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2414,6 +2418,59 @@ object SignalDatabaseMigrations {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS donation_receipt_type_index ON donation_receipt (receipt_type);")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS donation_receipt_date_index ON donation_receipt (receipt_date);")
|
||||
}
|
||||
|
||||
if (oldVersion < STORIES) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN is_story INTEGER DEFAULT 0")
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN parent_story_id INTEGER DEFAULT 0")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mms_is_story_index ON mms (is_story)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON mms (parent_story_id)")
|
||||
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN distribution_list_id INTEGER DEFAULT NULL")
|
||||
|
||||
db.execSQL(
|
||||
// language=sql
|
||||
"""
|
||||
CREATE TABLE distribution_list (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
distribution_id TEXT UNIQUE NOT NULL,
|
||||
recipient_id INTEGER UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
// language=sql
|
||||
"""
|
||||
CREATE TABLE distribution_list_member (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL,
|
||||
UNIQUE(list_id, recipient_id) ON CONFLICT IGNORE
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val recipientId = db.insert(
|
||||
"recipient", null,
|
||||
contentValuesOf(
|
||||
"distribution_list_id" to DistributionListId.MY_STORY_ID,
|
||||
"storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||
"profile_sharing" to 1
|
||||
)
|
||||
)
|
||||
|
||||
val listUUID = UUID.randomUUID().toString()
|
||||
db.insert(
|
||||
"distribution_list", null,
|
||||
contentValuesOf(
|
||||
"_id" to DistributionListId.MY_STORY_ID,
|
||||
"name" to listUUID,
|
||||
"distribution_id" to listUUID,
|
||||
"recipient_id" to recipientId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface DatabaseId {
|
||||
@NonNull String serialize();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A wrapper around the primary key of the distribution list database to provide strong typing.
|
||||
*/
|
||||
public final class DistributionListId implements DatabaseId, Parcelable {
|
||||
|
||||
public static final long MY_STORY_ID = 1L;
|
||||
public static final DistributionListId MY_STORY = DistributionListId.from(MY_STORY_ID);
|
||||
|
||||
private final long id;
|
||||
|
||||
public static @NonNull DistributionListId from(long id) {
|
||||
if (id <= 0) {
|
||||
throw new IllegalArgumentException("Invalid ID! " + id);
|
||||
}
|
||||
return new DistributionListId(id);
|
||||
}
|
||||
|
||||
public static @Nullable DistributionListId fromNullable(long id) {
|
||||
if (id > 0) {
|
||||
return new DistributionListId(id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DistributionListId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String serialize() {
|
||||
return String.valueOf(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "DistributionListId::" + id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final DistributionListId that = (DistributionListId) o;
|
||||
return id == that.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
public static final Creator<DistributionListId> CREATOR = new Creator<DistributionListId>() {
|
||||
@Override
|
||||
public DistributionListId createFromParcel(Parcel in) {
|
||||
return new DistributionListId(in.readLong());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DistributionListId[] newArray(int size) {
|
||||
return new DistributionListId[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
data class DistributionListPartialRecord(
|
||||
val id: DistributionListId,
|
||||
val name: CharSequence,
|
||||
val recipientId: RecipientId
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
|
||||
/**
|
||||
* Represents an entry in the [org.thoughtcrime.securesms.database.DistributionListDatabase].
|
||||
*/
|
||||
data class DistributionListRecord(
|
||||
val id: DistributionListId,
|
||||
val name: String,
|
||||
val distributionId: DistributionId,
|
||||
val members: List<RecipientId>
|
||||
)
|
||||
@@ -13,6 +13,18 @@ data class MessageId(
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns null for invalid IDs. Useful when pulling a possibly-unset ID from a database, or something like that.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromNullable(id: Long, mms: Boolean): MessageId? {
|
||||
return if (id > 0) {
|
||||
MessageId(id, mms)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deserialize(serialized: String): MessageId {
|
||||
val parts: List<String> = serialized.split("|")
|
||||
|
||||
@@ -34,6 +34,7 @@ data class RecipientRecord(
|
||||
val e164: String?,
|
||||
val email: String?,
|
||||
val groupId: GroupId?,
|
||||
val distributionListId: DistributionListId?,
|
||||
val groupType: RecipientDatabase.GroupType,
|
||||
val isBlocked: Boolean,
|
||||
val muteUntil: Long,
|
||||
@@ -70,6 +71,7 @@ data class RecipientRecord(
|
||||
val senderKeyCapability: Recipient.Capability,
|
||||
val announcementGroupCapability: Recipient.Capability,
|
||||
val changeNumberCapability: Recipient.Capability,
|
||||
val storiesCapability: Recipient.Capability,
|
||||
val insightsBannerTier: InsightsBannerTier,
|
||||
val storageId: ByteArray?,
|
||||
val mentionSetting: MentionSetting,
|
||||
|
||||
Reference in New Issue
Block a user