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:
Alex Hart
2022-02-24 13:40:28 -04:00
parent 765185952e
commit 174cd860a0
416 changed files with 19506 additions and 857 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull;
public interface DatabaseId {
@NonNull String serialize();
}

View File

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

View File

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

View File

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

View File

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

View File

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