Merge remote-tracking branch 'upstream/dev' into disappearing-messages

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
#	app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
#	app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
#	app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt
#	app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
#	app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
#	app/src/main/res/layout/activity_conversation_v2.xml
#	libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
#	libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt
#	libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
#	libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt
#	libsignal/protobuf/SignalService.proto
#	libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt
This commit is contained in:
0x330a
2023-07-18 11:39:03 +10:00
209 changed files with 8987 additions and 2664 deletions

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import androidx.core.content.contentValuesOf
import androidx.core.database.getBlobOrNull
import androidx.core.database.getLongOrNull
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
companion object {
private const val VARIANT = "variant"
private const val PUBKEY = "publicKey"
private const val DATA = "data"
private const val TIMESTAMP = "timestamp" // Milliseconds
private const val TABLE_NAME = "configs_table"
const val CREATE_CONFIG_TABLE_COMMAND =
"CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
}
fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
val db = writableDatabase
val contentValues = contentValuesOf(
VARIANT to variant,
PUBKEY to publicKey,
DATA to data,
TIMESTAMP to timestamp
)
db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
}
fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
val db = readableDatabase
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
return query?.use { cursor ->
if (!cursor.moveToFirst()) return@use null
val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null
bytes
}
}
fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
val db = readableDatabase
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
if (cursor == null) return 0
if (!cursor.moveToFirst()) return 0
return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0)
}
}

View File

@@ -36,9 +36,9 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
@SuppressWarnings("unused")
private static final String TAG = GroupDatabase.class.getSimpleName();
static final String TABLE_NAME = "groups";
public static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
public static final String GROUP_ID = "group_id";
private static final String TITLE = "title";
private static final String MEMBERS = "members";
private static final String ZOMBIE_MEMBERS = "zombie_members";
@@ -133,12 +133,12 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return new Reader(cursor);
}
public List<GroupRecord> getAllGroups() {
public List<GroupRecord> getAllGroups(boolean includeInactive) {
Reader reader = getGroups();
GroupRecord record;
List<GroupRecord> groups = new LinkedList<>();
while ((record = reader.getNext()) != null) {
if (record.isActive()) { groups.add(record); }
if (record.isActive() || includeInactive) { groups.add(record); }
}
reader.close();
return groups;

View File

@@ -458,9 +458,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
}
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
val database = databaseHelper.writableDatabase
val timestamp = Date().time.toString()
val index = "$groupPublicKey-$timestamp"
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()

View File

@@ -4,11 +4,8 @@ import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.JsonUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@@ -24,12 +21,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
}
fun getThreadID(hexEncodedPublicKey: String): Long {
val address = Address.fromSerialized(hexEncodedPublicKey)
val recipient = Recipient.from(context, address, false)
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
}
fun getAllOpenGroups(): Map<Long, OpenGroup> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
@@ -61,6 +52,13 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
}
}
fun getThreadId(openGroup: OpenGroup): Long? {
val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "$publicChat = ?", arrayOf(JsonUtil.toJson(openGroup.toJson()))) { cursor ->
cursor.getLong(threadID)
}
}
fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) {
if (threadID < 0) {
return

View File

@@ -21,13 +21,11 @@ import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract.CommonDataKinds.BaseTypes
import com.annimon.stream.Stream
import com.google.android.mms.pdu_alt.NotificationInd
import com.google.android.mms.pdu_alt.PduHeaders
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage
@@ -42,16 +40,13 @@ import org.session.libsession.utilities.Address.Companion.UNKNOWN
import org.session.libsession.utilities.Address.Companion.fromExternal
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.Contact
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
import org.session.libsession.utilities.IdentityKeyMismatch
import org.session.libsession.utilities.IdentityKeyMismatchList
import org.session.libsession.utilities.NetworkFailure
import org.session.libsession.utilities.NetworkFailureList
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
import org.session.libsession.utilities.Util.toIsoBytes
import org.session.libsession.utilities.Util.toIsoString
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientFormattingException
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils.queue
@@ -163,7 +158,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
)
get(context).groupReceiptDatabase()
.update(ourAddress, id, status, timestamp)
get(context).threadDatabase().update(threadId, false)
get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
}
}
@@ -206,25 +201,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
}
@Throws(RecipientFormattingException::class, MmsException::class)
private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long {
return if (retrieved.groupId != null) {
val groupRecipients = Recipient.from(
context,
retrieved.groupId,
true
)
get(context).threadDatabase().getOrCreateThreadIdFor(groupRecipients)
} else {
val sender = Recipient.from(
context,
retrieved.from,
true
)
get(context).threadDatabase().getOrCreateThreadIdFor(sender)
}
}
private fun rawQuery(where: String, arguments: Array<String>?): Cursor {
val database = databaseHelper.readableDatabase
return database.rawQuery(
@@ -266,7 +242,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
" WHERE " + ID + " = ?", arrayOf(id.toString() + "")
)
if (threadId.isPresent) {
get(context).threadDatabase().update(threadId.get(), false)
get(context).threadDatabase().update(threadId.get(), false, true)
}
}
@@ -323,10 +299,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val attachmentDatabase = get(context).attachmentDatabase()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(messageId) })
val threadId = getThreadIdForMessage(messageId)
if (!read) {
val mentionChange = if (hasMention) { 1 } else { 0 }
get(context).threadDatabase().decrementUnread(threadId, 1, mentionChange)
}
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId)
}
@@ -350,6 +323,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(id.toString()))
}
fun setMessagesRead(threadId: Long, beforeTime: Long): List<MarkedMessageInfo> {
return setMessagesRead(
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?",
arrayOf(threadId.toString(), beforeTime.toString())
)
}
fun setMessagesRead(threadId: Long): List<MarkedMessageInfo> {
return setMessagesRead(
THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)",
@@ -576,18 +556,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
contentLocation: String,
threadId: Long, mailbox: Long,
serverTimestamp: Long,
runIncrement: Boolean,
runThreadUpdate: Boolean
): Optional<InsertResult> {
var threadId = threadId
if (threadId == -1L || retrieved.isGroupMessage) {
try {
threadId = getThreadIdFor(retrieved)
} catch (e: RecipientFormattingException) {
Log.w("MmsDatabase", e)
if (threadId == -1L) throw MmsException(e)
}
}
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
deleteExpirationTimerMessages(threadId)
val contentValues = ContentValues()
contentValues.put(DATE_SENT, retrieved.sentTimeMillis)
@@ -642,12 +613,8 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
null,
)
if (!MmsSmsColumns.Types.isExpirationTimerUpdate(mailbox)) {
if (runIncrement) {
val mentionAmount = if (retrieved.hasMention()) { 1 } else { 0 }
get(context).threadDatabase().incrementUnread(threadId, 1, mentionAmount)
}
if (runThreadUpdate) {
get(context).threadDatabase().update(threadId, true)
get(context).threadDatabase().update(threadId, true, true)
}
}
notifyConversationListeners(threadId)
@@ -661,27 +628,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
serverTimestamp: Long,
runThreadUpdate: Boolean
): Optional<InsertResult> {
var threadId = threadId
if (threadId == -1L) {
if (retrieved.isGroup) {
val decodedGroupId: String = if (retrieved is OutgoingExpirationUpdateMessage) {
retrieved.groupId
} else {
(retrieved as OutgoingGroupMediaMessage).groupId
}
val groupId: String
groupId = try {
doubleEncodeGroupID(decodedGroupId)
} catch (e: IOException) {
Log.e(TAG, "Couldn't encrypt group ID")
throw MmsException(e)
}
val group = Recipient.from(context, fromSerialized(groupId), false)
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(group)
} else {
threadId = get(context).threadDatabase().getOrCreateThreadIdFor(retrieved.recipient)
}
}
if (threadId < 0 ) throw MmsException("No thread ID supplied!")
deleteExpirationTimerMessages(threadId)
val messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp, runThreadUpdate)
if (messageId == -1L) {
@@ -697,7 +644,6 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
retrieved: IncomingMediaMessage,
threadId: Long,
serverTimestamp: Long = 0,
runIncrement: Boolean,
runThreadUpdate: Boolean
): Optional<InsertResult> {
var type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT
@@ -716,7 +662,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
if (retrieved.isMessageRequestResponse) {
type = type or MmsSmsColumns.Types.MESSAGE_REQUEST_RESPONSE_BIT
}
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runIncrement, runThreadUpdate)
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp, runThreadUpdate)
}
@JvmOverloads
@@ -806,10 +752,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
)
}
with (get(context).threadDatabase()) {
setLastSeen(threadId)
val lastSeen = getLastSeenAndHasSent(threadId).first()
if (lastSeen < message.sentTimeMillis) {
setLastSeen(threadId, message.sentTimeMillis)
}
setHasSent(threadId, true)
if (runThreadUpdate) {
update(threadId, true)
update(threadId, true, true)
}
}
return messageId
@@ -944,7 +893,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
groupReceiptDatabase.deleteRowsForMessage(messageId)
val database = databaseHelper.writableDatabase
database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
val threadDeleted = get(context).threadDatabase().update(threadId, false)
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
notifyStickerListeners()
notifyStickerPackListeners()
@@ -961,7 +910,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val database = databaseHelper.writableDatabase
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
val threadDeleted = get(context).threadDatabase().update(threadId, false)
val threadDeleted = get(context).threadDatabase().update(threadId, false, true)
notifyConversationListeners(threadId)
notifyStickerListeners()
notifyStickerPackListeners()
@@ -1159,7 +1108,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
}
val threadDb = get(context).threadDatabase()
for (threadId in threadIds) {
val threadDeleted = threadDb.update(threadId, false)
val threadDeleted = threadDb.update(threadId, false, true)
notifyConversationListeners(threadId)
}
notifyStickerListeners()

View File

@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms.database;
import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX;
import android.content.Context;
import android.database.Cursor;
@@ -25,6 +27,7 @@ import androidx.annotation.Nullable;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
import org.jetbrains.annotations.NotNull;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
@@ -36,6 +39,8 @@ import java.io.Closeable;
import java.util.HashSet;
import java.util.Set;
import kotlin.Pair;
public class MmsSmsDatabase extends Database {
@SuppressWarnings("unused")
@@ -259,8 +264,8 @@ public class MmsSmsDatabase extends Database {
return -1;
}
public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address, boolean reverse) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
@@ -512,6 +517,23 @@ public class MmsSmsDatabase extends Database {
return new Reader(cursor);
}
@NotNull
public Pair<Boolean, Long> timestampAndDirectionForCurrent(@NotNull Cursor cursor) {
int sentColumn = cursor.getColumnIndex(MmsSmsColumns.NORMALIZED_DATE_SENT);
String msgType = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));
long sentTime = cursor.getLong(sentColumn);
long type = 0;
if (MmsSmsDatabase.MMS_TRANSPORT.equals(msgType)) {
int typeIndex = cursor.getColumnIndex(MESSAGE_BOX);
type = cursor.getLong(typeIndex);
} else if (MmsSmsDatabase.SMS_TRANSPORT.equals(msgType)) {
int typeIndex = cursor.getColumnIndex(SmsDatabase.TYPE);
type = cursor.getLong(typeIndex);
}
return new Pair<Boolean, Long>(MmsSmsColumns.Types.isOutgoingMessageType(type), sentTime);
}
public class Reader implements Closeable {
private final Cursor cursor;

View File

@@ -63,13 +63,14 @@ public class RecipientDatabase extends Database {
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String NOTIFY_TYPE = "notify_type"; // all, mentions only, none
private static final String WRAPPER_HASH = "wrapper_hash";
private static final String[] RECIPIENT_PROJECTION = new String[] {
BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE
FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH
};
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@@ -142,6 +143,11 @@ public class RecipientDatabase extends Database {
"ADD COLUMN " + DISAPPEARING_STATE + " INTEGER DEFAULT 0;";
}
public static String getAddWrapperHash() {
return "ALTER TABLE "+TABLE_NAME+" "+
"ADD COLUMN "+WRAPPER_HASH+" TEXT DEFAULT NULL;";
}
public static final int NOTIFY_TYPE_ALL = 0;
public static final int NOTIFY_TYPE_MENTIONS = 1;
public static final int NOTIFY_TYPE_NONE = 2;
@@ -160,18 +166,14 @@ public class RecipientDatabase extends Database {
public Optional<RecipientSettings> getRecipientSettings(@NonNull Address address) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null);
try (Cursor cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return getRecipientSettings(cursor);
}
return Optional.absent();
} finally {
if (cursor != null) cursor.close();
}
}
@@ -201,6 +203,7 @@ public class RecipientDatabase extends Database {
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
String wrapperHash = cursor.getString(cursor.getColumnIndexOrThrow(WRAPPER_HASH));
MaterialColor color;
byte[] profileKey = null;
@@ -233,7 +236,7 @@ public class RecipientDatabase extends Database {
systemPhoneLabel, systemContactUri,
signalProfileName, signalProfileAvatar, profileSharing,
notificationChannel, Recipient.UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection));
forceSmsSelection, wrapperHash));
}
public void setColor(@NonNull Recipient recipient, @NonNull MaterialColor color) {
@@ -260,6 +263,24 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners();
}
public boolean getApproved(@NonNull Address address) {
SQLiteDatabase db = getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[]{APPROVED}, ADDRESS + " = ?", new String[]{address.serialize()}, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return cursor.getInt(cursor.getColumnIndexOrThrow(APPROVED)) == 1;
}
}
return false;
}
public void setRecipientHash(@NonNull Recipient recipient, String recipientHash) {
ContentValues values = new ContentValues();
values.put(WRAPPER_HASH, recipientHash);
updateOrInsert(recipient.getAddress(), values);
recipient.resolve().setWrapperHash(recipientHash);
notifyRecipientListeners();
}
public void setApproved(@NonNull Recipient recipient, boolean approved) {
ContentValues values = new ContentValues();
values.put(APPROVED, approved ? 1 : 0);
@@ -276,14 +297,6 @@ public class RecipientDatabase extends Database {
notifyRecipientListeners();
}
public void setBlocked(@NonNull Recipient recipient, boolean blocked) {
ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0);
updateOrInsert(recipient.getAddress(), values);
recipient.resolve().setBlocked(blocked);
notifyRecipientListeners();
}
public void setBlocked(@NonNull Iterable<Recipient> recipients, boolean blocked) {
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();

View File

@@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import androidx.core.database.getStringOrNull
import android.database.Cursor
import androidx.core.database.getStringOrNull
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@@ -43,6 +45,9 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
val database = databaseHelper.readableDatabase
return database.getAll(sessionContactTable, null, null) { cursor ->
contactFromCursor(cursor)
}.filter { contact ->
val sessionId = SessionId(contact.sessionID)
sessionId.prefix == IdPrefix.STANDARD
}.toSet()
}

View File

@@ -93,6 +93,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun cancelPendingMessageSendJobs(threadID: Long) {
val database = databaseHelper.writableDatabase
val attachmentUploadJobKeys = mutableListOf<String>()
database.beginTransaction()
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
val job = jobFromCursor(cursor) as AttachmentUploadJob?
if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) }
@@ -103,15 +104,19 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) }
}
if (attachmentUploadJobKeys.isNotEmpty()) {
val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ")
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString ))
attachmentUploadJobKeys.forEach {
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
arrayOf( AttachmentUploadJob.KEY, it ))
}
}
if (messageSendJobKeys.isNotEmpty()) {
val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ")
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)",
arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString ))
messageSendJobKeys.forEach {
database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} = ?",
arrayOf( MessageSendJob.KEY, it ))
}
}
database.setTransactionSuccessful()
database.endTransaction()
}
fun isJobCanceled(job: Job): Boolean {

View File

@@ -149,7 +149,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(id);
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
}
@@ -235,10 +235,6 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(BODY, "");
contentValues.put(HAS_MENTION, 0);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
long threadId = getThreadIdForMessage(messageId);
if (!read) {
DatabaseComponent.get(context).threadDatabase().decrementUnread(threadId, 1, (hasMention ? 1 : 0));
}
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
}
@@ -257,7 +253,7 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(id);
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
}
@@ -320,7 +316,7 @@ public class SmsDatabase extends MessagingDatabase {
ID + " = ?",
new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))});
DatabaseComponent.get(context).threadDatabase().update(threadId, false);
DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
foundMessage = true;
}
@@ -338,6 +334,9 @@ public class SmsDatabase extends MessagingDatabase {
}
}
public List<MarkedMessageInfo> setMessagesRead(long threadId, long beforeTime) {
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1) AND " + DATE_SENT + " <= ?", new String[]{threadId+"", beforeTime+""});
}
public List<MarkedMessageInfo> setMessagesRead(long threadId) {
return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)});
}
@@ -399,14 +398,14 @@ public class SmsDatabase extends MessagingDatabase {
long threadId = getThreadIdForMessage(messageId);
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
notifyConversationListeners(threadId);
notifyConversationListListeners();
return new Pair<>(messageId, threadId);
}
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
protected Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type, long serverTimestamp, boolean runThreadUpdate) {
Recipient recipient = Recipient.from(context, message.getSender(), true);
Recipient groupRecipient;
@@ -475,12 +474,8 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
if (unread && runIncrement) {
DatabaseComponent.get(context).threadDatabase().incrementUnread(threadId, 1, (message.hasMention() ? 1 : 0));
}
if (runThreadUpdate) {
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
}
if (message.getSubscriptionId() != -1) {
@@ -512,16 +507,16 @@ public class SmsDatabase extends MessagingDatabase {
return typeMask;
}
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runIncrement, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runIncrement, runThreadUpdate);
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, 0, runThreadUpdate);
}
public Optional<InsertResult> insertCallMessage(IncomingTextMessage message) {
return insertMessageInbox(message, 0, 0, true, true);
return insertMessageInbox(message, 0, 0, true);
}
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runIncrement, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runIncrement, runThreadUpdate);
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp, runThreadUpdate);
}
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp, boolean runThreadUpdate) {
@@ -576,9 +571,12 @@ public class SmsDatabase extends MessagingDatabase {
}
if (runThreadUpdate) {
DatabaseComponent.get(context).threadDatabase().update(threadId, true);
DatabaseComponent.get(context).threadDatabase().update(threadId, true, true);
}
long lastSeen = DatabaseComponent.get(context).threadDatabase().getLastSeenAndHasSent(threadId).first();
if (lastSeen < message.getSentTimestampMillis()) {
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId, message.getSentTimestampMillis());
}
DatabaseComponent.get(context).threadDatabase().setLastSeen(threadId);
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true);
@@ -630,7 +628,7 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
return threadDeleted;
}
@@ -654,7 +652,7 @@ public class SmsDatabase extends MessagingDatabase {
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
argValues
);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false, true);
notifyConversationListeners(threadId);
return threadDeleted;
}

View File

@@ -2,6 +2,18 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.BlindedIdMapping
@@ -14,6 +26,16 @@ import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
import org.session.libsession.messaging.jobs.ConfigurationSyncJob
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
@@ -35,11 +57,14 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupRecord
@@ -48,25 +73,105 @@ import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.ClosedGroupManager
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import java.security.MessageDigest
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
open class Storage(context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory) : Database(context, helper), StorageProtocol,
ThreadDatabase.ConversationThreadUpdateListener {
override fun threadCreated(address: Address, threadId: Long) {
val localUserAddress = getUserPublicKey() ?: return
if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests
val volatile = configFactory.convoVolatile ?: return
if (address.isGroup) {
val groups = configFactory.userGroups ?: return
if (address.isClosedGroup) {
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
val closedGroup = getGroup(address.toGroupString())
if (closedGroup != null && closedGroup.isActive) {
val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId)
groups.set(legacyGroup)
val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy(
lastRead = SnodeAPI.nowWithOffset,
)
volatile.set(newVolatileParams)
}
} else if (address.isOpenGroup) {
// these should be added on the group join / group info fetch
Log.w("Loki", "Thread created called for open group address, not adding any extra information")
}
} else if (address.isContact) {
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
// don't update our own address into the contacts DB
if (getUserPublicKey() != address.serialize()) {
val contacts = configFactory.contacts ?: return
contacts.upsertContact(address.serialize()) {
priority = ConfigBase.PRIORITY_VISIBLE
}
} else {
val userProfile = configFactory.user ?: return
userProfile.setNtsPriority(ConfigBase.PRIORITY_VISIBLE)
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true)
}
val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize())
volatile.set(newVolatileParams)
}
}
override fun threadDeleted(address: Address, threadId: Long) {
val volatile = configFactory.convoVolatile ?: return
if (address.isGroup) {
val groups = configFactory.userGroups ?: return
if (address.isClosedGroup) {
val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize())
volatile.eraseLegacyClosedGroup(sessionId)
groups.eraseLegacyGroup(sessionId)
} else if (address.isOpenGroup) {
// these should be removed in the group leave / handling new configs
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
}
} else {
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return
volatile.eraseOneToOne(address.serialize())
if (getUserPublicKey() != address.serialize()) {
val contacts = configFactory.contacts ?: return
contacts.upsertContact(address.serialize()) {
priority = PRIORITY_HIDDEN
}
} else {
val userProfile = configFactory.user ?: return
userProfile.setNtsPriority(PRIORITY_HIDDEN)
}
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
override fun getUserPublicKey(): String? {
return TextSecurePreferences.getLocalNumber(context)
}
@@ -87,6 +192,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
database.setProfileAvatar(recipient, profileAvatar)
}
override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) {
val db = DatabaseComponent.get(context).recipientDatabase()
db.setProfileAvatar(recipient, newProfilePicture)
db.setProfileKey(recipient, newProfileKey)
}
override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) {
val ourRecipient = fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false)
}
ourRecipient.resolve().profileKey = newProfileKey
TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) })
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
if (newProfileKey != null) {
JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address))
}
}
override fun getOrGenerateRegistrationID(): Int {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) {
@@ -107,19 +231,56 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getAttachmentsForMessage(messageID)
}
override fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean) {
override fun getLastSeen(threadId: Long): Long {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.setRead(threadId, updateLastSeen)
return threadDb.getLastSeenAndHasSent(threadId)?.first() ?: 0L
}
override fun incrementUnread(threadId: Long, amount: Int, unreadMentionAmount: Int) {
override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.incrementUnread(threadId, amount, unreadMentionAmount)
getRecipientForThread(threadId)?.let { recipient ->
val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first()
// don't set the last read in the volatile if we didn't set it in the DB
if (!threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime, force) && !force) return
// don't process configs for inbox recipients
if (recipient.isOpenGroupInboxRecipient) return
configFactory.convoVolatile?.let { config ->
val convo = when {
// recipient closed group
recipient.isClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
// recipient is open group
recipient.isOpenGroupRecipient -> {
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
config.getOrConstructCommunity(base, room, pubKey)
} ?: return
}
// otherwise recipient is one to one
recipient.isContactRecipient -> {
// don't process non-standard session IDs though
val sessionId = SessionId(recipient.address.serialize())
if (sessionId.prefix != IdPrefix.STANDARD) return
config.getOrConstructOneToOne(recipient.address.serialize())
}
else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}")
}
convo.lastRead = lastSeenTime
if (convo.unread) {
convo.unread = lastSeenTime <= currentLastRead
notifyConversationListListeners()
}
config.set(convo)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
}
override fun updateThread(threadId: Long, unarchive: Boolean) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.update(threadId, unarchive)
threadDb.update(threadId, unarchive, false)
}
override fun persist(message: VisibleMessage,
@@ -128,7 +289,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
groupPublicKey: String?,
openGroupID: String?,
attachments: List<Attachment>,
runIncrement: Boolean,
runThreadUpdate: Boolean): Long? {
var messageID: Long? = null
val senderAddress = fromSerialized(message.sender!!)
@@ -155,13 +315,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
val targetRecipient = Recipient.from(context, targetAddress, false)
if (!targetRecipient.isGroupRecipient) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
if (isUserSender || isUserBlindedSender) {
recipientDb.setApproved(targetRecipient, true)
setRecipientApproved(targetRecipient, true)
} else {
recipientDb.setApprovedMe(targetRecipient, true)
setRecipientApprovedMe(targetRecipient, true)
}
}
if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) {
// open group recipients should explicitly create threads
message.threadID = getOrCreateThreadIdFor(targetAddress)
}
val expirationConfig = getExpirationConfiguration(message.threadID ?: -1)
val expiresInMillis = (expirationConfig?.durationSeconds ?: 0) * 1000L
val expireStartedAt = if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) message.sentTimestamp!! else 0
@@ -186,7 +349,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
it.toSignalPointer()
}
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, expiresInMillis, expireStartedAt, group, signalServiceAttachments, quote, linkPreviews)
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID!!, message.receivedTimestamp ?: 0, runThreadUpdate)
}
if (insertResult.isPresent) {
messageID = insertResult.get().messageId
@@ -203,7 +366,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp, expiresInMillis, expireStartedAt)
else IncomingTextMessage.from(message, senderAddress, group, expiresInMillis, expireStartedAt)
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runIncrement, runThreadUpdate)
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runThreadUpdate)
}
insertResult.orNull()?.let { result ->
messageID = result.messageId
@@ -252,6 +415,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId)
}
override fun getConfigSyncJob(destination: Destination): Job? {
return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull {
(it as? ConfigurationSyncJob)?.destination == destination
}
}
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return
JobQueue.shared.resumePendingSendMessage(job)
@@ -261,11 +430,201 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job)
}
override fun cancelPendingMessageSendJobs(threadID: Long) {
val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
jobDb.cancelPendingMessageSendJobs(threadID)
}
override fun getAuthToken(room: String, server: String): String? {
val id = "$server.$room"
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id)
}
override fun notifyConfigUpdates(forConfigObject: ConfigBase) {
notifyUpdates(forConfigObject)
}
override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean {
return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly)
}
override fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
return configFactory.canPerformChange(variant, publicKey, changeTimestampMs)
}
fun notifyUpdates(forConfigObject: ConfigBase) {
when (forConfigObject) {
is UserProfile -> updateUser(forConfigObject)
is Contacts -> updateContacts(forConfigObject)
is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject)
is UserGroupsConfig -> updateUserGroups(forConfigObject)
}
}
private fun updateUser(userProfile: UserProfile) {
val userPublicKey = getUserPublicKey() ?: return
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// update name
val name = userProfile.getName() ?: return
val userPic = userProfile.getPic()
val profileManager = SSKEnvironment.shared.profileManager
if (name.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, name)
profileManager.setName(context, recipient, name)
}
// update pfp
if (userPic == UserPic.DEFAULT) {
clearUserPic()
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url) {
setUserProfilePicture(userPic.url, userPic.key)
}
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
// delete nts thread if needed
val ourThread = getThreadId(recipient) ?: return
deleteConversation(ourThread)
} else {
// create note to self thread if needed (?)
val ourThread = getOrCreateThreadIdFor(recipient.address)
DatabaseComponent.get(context).threadDatabase().setHasSent(ourThread, true)
setPinned(ourThread, userProfile.getNtsPriority() > 0)
}
}
private fun updateContacts(contacts: Contacts) {
val extracted = contacts.all().toList()
addLibSessionContacts(extracted)
}
override fun clearUserPic() {
val userPublicKey = getUserPublicKey() ?: return
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
// would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
// clear picture if userPic is null
TextSecurePreferences.setProfileKey(context, null)
ProfileKeyUtil.setEncodedProfileKey(context, null)
recipientDatabase.setProfileAvatar(recipient, null)
TextSecurePreferences.setProfileAvatarId(context, 0)
TextSecurePreferences.setProfilePictureURL(context, null)
Recipient.removeCached(fromSerialized(userPublicKey))
configFactory.user?.setPic(UserPic.DEFAULT)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
private fun updateConvoVolatile(convos: ConversationVolatileConfig) {
val extracted = convos.all()
for (conversation in extracted) {
val threadId = when (conversation) {
is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false)
is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false)
is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
}
if (threadId != null) {
if (conversation.lastRead > getLastSeen(threadId)) {
markConversationAsRead(threadId, conversation.lastRead, force = true)
}
updateThread(threadId, false)
}
}
}
private fun updateUserGroups(userGroups: UserGroupsConfig) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
val localUserPublicKey = getUserPublicKey() ?: return Log.w(
"Loki",
"No user public key when trying to update user groups from config"
)
val communities = userGroups.allCommunityInfo()
val lgc = userGroups.allLegacyGroupInfo()
val allOpenGroups = getAllOpenGroups()
val toDeleteCommunities = allOpenGroups.filter {
Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() }
}
val existingCommunities: Map<Long, OpenGroup> = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys }
val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } }
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isClosedGroup }
val lgcIds = lgc.map { it.sessionId }
val toDeleteClosedGroups = existingClosedGroups.filter { group ->
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
}
// delete the ones which are not listed in the config
toDeleteCommunities.values.forEach { openGroup ->
OpenGroupManager.delete(openGroup.server, openGroup.room, context)
}
toDeleteClosedGroups.forEach { deleteGroup ->
val threadId = getThreadId(deleteGroup.encodedId)
if (threadId != null) {
ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true)
}
}
toAddCommunities.forEach { toAddCommunity ->
val joinUrl = toAddCommunity.community.fullUrl()
if (!hasBackgroundGroupAddJob(joinUrl)) {
JobQueue.shared.add(BackgroundGroupAddJob(joinUrl))
}
}
for (groupInfo in communities) {
val groupBaseCommunity = groupInfo.community
if (groupBaseCommunity.fullUrl() in existingJoinUrls) {
// add it
val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() }
threadDb.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED)
}
}
for (group in lgc) {
val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId }
val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) }
if (existingGroup != null) {
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
ClosedGroupManager.silentlyRemoveGroup(context,existingThread,GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true)
} else if (existingThread == null) {
Log.w("Loki-DBG", "Existing group had no thread to hide")
} else {
Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}")
threadDb.setPinned(existingThread, group.priority == PRIORITY_PINNED)
}
} else {
val members = group.members.keys.map { Address.fromSerialized(it) }
val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) }
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
val title = group.name
val formationTimestamp = (group.joinedAt * 1000L)
createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
setProfileSharing(Address.fromSerialized(groupId), true)
// Add the group to the user's set of public keys to poll for
addClosedGroupPublicKey(group.sessionId)
// Store the encryption key pair
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
// Set expiration timer
val expireTimer = group.disappearingTimer
setExpirationTimer(groupId, expireTimer.toInt())
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
// Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
threadDb.setDate(threadID, formationTimestamp)
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
// Don't create config group here, it's from a config update
// Start polling
ClosedGroupPollerV2.shared.startPolling(group.sessionId)
}
}
}
override fun setAuthToken(room: String, server: String, newValue: String) {
val id = "$server.$room"
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue)
@@ -501,6 +860,59 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp)
}
override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) {
val volatiles = configFactory.convoVolatile ?: return
val userGroups = configFactory.userGroups ?: return
val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey)
groupVolatileConfig.lastRead = formationTimestamp
volatiles.set(groupVolatileConfig)
val groupInfo = GroupInfo.LegacyGroupInfo(
sessionId = groupPublicKey,
name = name,
members = members,
priority = ConfigBase.PRIORITY_VISIBLE,
encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = encryptionKeyPair.privateKey.serialize(),
disappearingTimer = 0L,
joinedAt = (formationTimestamp / 1000L)
)
// shouldn't exist, don't use getOrConstruct + copy
userGroups.set(groupInfo)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
override fun updateGroupConfig(groupPublicKey: String) {
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val groupAddress = fromSerialized(groupID)
// TODO: probably add a check in here for isActive?
// TODO: also check if local user is a member / maybe run delete otherwise?
val existingGroup = getGroup(groupID)
?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config")
val userGroups = configFactory.userGroups ?: return
if (!existingGroup.isActive) {
userGroups.eraseLegacyGroup(groupPublicKey)
return
}
val name = existingGroup.title
val admins = existingGroup.admins.map { it.serialize() }
val members = existingGroup.members.map { it.serialize() }
val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members)
val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config")
val recipientSettings = getRecipientSettings(groupAddress) ?: return
val threadID = getThreadId(groupAddress) ?: return
val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy(
name = name,
members = membersMap,
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
encSecKey = latestKeyPair.privateKey.serialize(),
priority = if (isPinned(threadID)) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
disappearingTimer = recipientSettings.expireMessages.toLong(),
joinedAt = (existingGroup.formationTimestamp / 1000L)
)
userGroups.set(groupInfo)
}
override fun isGroupActive(groupPublicKey: String): Boolean {
return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true
}
@@ -536,7 +948,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase()
smsDB.insertMessageInbox(infoMessage, true, true)
smsDB.insertMessageInbox(infoMessage, true)
if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
@@ -593,8 +1005,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey)
}
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp)
}
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
@@ -611,6 +1023,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.updateTimestampUpdated(groupID, updatedTimestamp)
}
override fun setExpirationTimer(address: String, duration: Int) {
val recipient = Recipient.from(context, fromSerialized(address), false)
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration)
if (recipient.isContactRecipient && !recipient.isLocalNumber) {
configFactory.contacts?.upsertContact(address) {
this.expiryMode = if (duration != 0) {
ExpiryMode.AfterRead(duration.toLong())
} else { // = 0 / delete
ExpiryMode.NONE
}
}
if (configFactory.contacts?.needsPush() == true) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
override fun setExpirationTimer(groupID: String, duration: Int) {
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
@@ -635,16 +1062,29 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
OpenGroupManager.updateOpenGroup(openGroup, context)
}
override fun getAllGroups(): List<GroupRecord> {
return DatabaseComponent.get(context).groupDatabase().allGroups
override fun getAllGroups(includeInactive: Boolean): List<GroupRecord> {
return DatabaseComponent.get(context).groupDatabase().getAllGroups(includeInactive)
}
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
return OpenGroupManager.addOpenGroup(urlAsString, context)
}
override fun onOpenGroupAdded(server: String) {
override fun onOpenGroupAdded(server: String, room: String) {
OpenGroupManager.restartPollerForServer(server.removeSuffix("/"))
val groups = configFactory.userGroups ?: return
val volatileConfig = configFactory.convoVolatile ?: return
val openGroup = getOpenGroup(room, server) ?: return
val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
val pubKeyHex = Hex.toStringCondensed(pubKey)
val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex)
groups.set(communityInfo)
val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey)
if (volatile.lastRead != 0L) {
val threadId = getThreadId(openGroup) ?: return
markConversationAsRead(threadId, volatile.lastRead, force = true)
}
volatileConfig.set(volatile)
}
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
@@ -662,17 +1102,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
}
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? {
val database = DatabaseComponent.get(context).threadDatabase()
return if (!openGroupID.isNullOrEmpty()) {
val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
database.getThreadIdIfExistsFor(recipient)
database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
} else if (!groupPublicKey.isNullOrEmpty()) {
val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
database.getOrCreateThreadIdFor(recipient)
if (createThread) database.getOrCreateThreadIdFor(recipient)
else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
} else {
val recipient = Recipient.from(context, fromSerialized(publicKey), false)
database.getOrCreateThreadIdFor(recipient)
if (createThread) database.getOrCreateThreadIdFor(recipient)
else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it }
}
}
@@ -681,6 +1123,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return getThreadId(address)
}
override fun getThreadId(openGroup: OpenGroup): Long? {
return GroupManager.getOpenGroupThreadID("${openGroup.server.removeSuffix("/")}.${openGroup.room}", context)
}
override fun getThreadId(address: Address): Long? {
val recipient = Recipient.from(context, address, false)
return getThreadId(recipient)
@@ -710,6 +1156,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun setContact(contact: Contact) {
DatabaseComponent.get(context).sessionContactDatabase().setContact(contact)
val address = fromSerialized(contact.sessionID)
if (!getRecipientApproved(address)) return
val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact)
val recipient = Recipient.from(context, address, false)
setRecipientHash(recipient, recipientHash)
}
override fun getRecipientForThread(threadId: Long): Recipient? {
@@ -721,6 +1172,51 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
}
override fun addLibSessionContacts(contacts: List<LibSessionContact>) {
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
val moreContacts = contacts.filter { contact ->
val id = SessionId(contact.id)
id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null }
}
val profileManager = SSKEnvironment.shared.profileManager
moreContacts.forEach { contact ->
val address = fromSerialized(contact.id)
val recipient = Recipient.from(context, address, false)
setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true)
setRecipientApproved(recipient, contact.approved)
setRecipientApprovedMe(recipient, contact.approvedMe)
if (contact.name.isNotEmpty()) {
profileManager.setName(context, recipient, contact.name)
} else {
profileManager.setName(context, recipient, null)
}
if (contact.nickname.isNotEmpty()) {
profileManager.setNickname(context, recipient, contact.nickname)
} else {
profileManager.setNickname(context, recipient, null)
}
if (contact.profilePicture != UserPic.DEFAULT) {
val (url, key) = contact.profilePicture
if (key.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach
profileManager.setProfilePicture(context, recipient, url, key)
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
} else {
profileManager.setProfilePicture(context, recipient, null, null)
}
if (contact.priority == PRIORITY_HIDDEN) {
getThreadId(fromSerialized(contact.id))?.let { conversationThreadId ->
deleteConversation(conversationThreadId)
}
} else {
getThreadId(fromSerialized(contact.id))?.let { conversationThreadId ->
setPinned(conversationThreadId, contact.priority == PRIORITY_PINNED)
}
}
setRecipientHash(recipient, contact.hashCode().toString())
}
}
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
val recipientDatabase = DatabaseComponent.get(context).recipientDatabase()
val threadDatabase = DatabaseComponent.get(context).threadDatabase()
@@ -744,19 +1240,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
recipientDatabase.setProfileSharing(recipient, true)
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
// create Thread if needed
val threadId = threadDatabase.getOrCreateThreadIdFor(recipient)
val threadId = threadDatabase.getThreadIdIfExistsFor(recipient)
if (contact.didApproveMe == true) {
recipientDatabase.setApprovedMe(recipient, true)
}
if (contact.isApproved == true) {
recipientDatabase.setApproved(recipient, true)
if (contact.isApproved == true && threadId != -1L) {
setRecipientApproved(recipient, true)
threadDatabase.setHasSent(threadId, true)
}
val contactIsBlocked: Boolean? = contact.isBlocked
if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) {
recipientDatabase.setBlocked(recipient, contactIsBlocked)
threadDatabase.deleteConversation(threadId)
setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true)
}
}
if (contacts.isNotEmpty()) {
@@ -764,6 +1259,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun setRecipientHash(recipient: Recipient, recipientHash: String?) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
recipientDb.setRecipientHash(recipient, recipientHash)
}
override fun getLastUpdated(threadID: Long): Long {
val threadDB = DatabaseComponent.get(context).threadDatabase()
return threadDB.getLastUpdated(threadID)
@@ -784,12 +1284,78 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return mmsSmsDb.getConversationCount(threadID)
}
override fun deleteConversation(threadId: Long) {
override fun setPinned(threadID: Long, isPinned: Boolean) {
val threadDB = DatabaseComponent.get(context).threadDatabase()
threadDB.deleteConversation(threadId)
threadDB.setPinned(threadID, isPinned)
val threadRecipient = getRecipientForThread(threadID) ?: return
if (threadRecipient.isLocalNumber) {
val user = configFactory.user ?: return
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE)
} else if (threadRecipient.isContactRecipient) {
val contacts = configFactory.contacts ?: return
contacts.upsertContact(threadRecipient.address.serialize()) {
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
}
} else if (threadRecipient.isGroupRecipient) {
val groups = configFactory.userGroups ?: return
if (threadRecipient.isClosedGroupRecipient) {
val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize())
val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy (
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
)
groups.set(newGroupInfo)
} else if (threadRecipient.isOpenGroupRecipient) {
val openGroup = getOpenGroup(threadID) ?: return
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy (
priority = if (isPinned) PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE
)
groups.set(newGroupInfo)
}
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
override fun isPinned(threadID: Long): Boolean {
val threadDB = DatabaseComponent.get(context).threadDatabase()
return threadDB.isPinned(threadID)
}
override fun setThreadDate(threadId: Long, newDate: Long) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
threadDb.setDate(threadId, newDate)
}
override fun deleteConversation(threadID: Long) {
val recipient = getRecipientForThread(threadID)
val threadDB = DatabaseComponent.get(context).threadDatabase()
val groupDB = DatabaseComponent.get(context).groupDatabase()
threadDB.deleteConversation(threadID)
if (recipient != null) {
if (recipient.isContactRecipient) {
if (recipient.isLocalNumber) return
val contacts = configFactory.contacts ?: return
contacts.upsertContact(recipient.address.serialize()) {
this.priority = PRIORITY_HIDDEN
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} else if (recipient.isClosedGroupRecipient) {
// TODO: handle closed group
val volatile = configFactory.convoVolatile ?: return
val groups = configFactory.userGroups ?: return
val groupID = recipient.address.toGroupString()
val closedGroup = getGroup(groupID)
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
if (closedGroup != null) {
groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it)
volatile.eraseLegacyClosedGroup(groupPublicKey)
groups.eraseLegacyGroup(groupPublicKey)
} else {
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
}
}
}
}
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentDataUri(attachmentId)
@@ -805,7 +1371,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val recipient = Recipient.from(context, address, false)
if (recipient.isBlocked) return
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
val threadId = getThreadId(recipient) ?: return
val expirationConfig = getExpirationConfiguration(threadId)
val expiresInMillis = (expirationConfig?.durationSeconds ?: 0) * 100L
val expireStartedAt = if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) sentTimestamp else 0
@@ -828,7 +1394,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.of(message)
)
database.insertSecureDecryptedMessageInbox(mediaMessage, -1, runIncrement = true, runThreadUpdate = true)
database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true)
if (expirationConfig?.expirationType == ExpirationType.DELETE_AFTER_SEND) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
@@ -838,7 +1404,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val userPublicKey = getUserPublicKey()
val senderPublicKey = response.sender!!
val recipientPublicKey = response.recipient!!
if (userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)) return
if (
userPublicKey == null
|| (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey)
// this is true if it is a sync message
|| (userPublicKey == recipientPublicKey && userPublicKey == senderPublicKey)
) return
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
val threadDB = DatabaseComponent.get(context).threadDatabase()
if (userPublicKey == senderPublicKey) {
@@ -850,7 +1423,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
val smsDb = DatabaseComponent.get(context).smsDatabase()
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
val threadId = threadDB.getOrCreateThreadIdFor(sender)
val threadId = getOrCreateThreadIdFor(sender.address)
val profile = response.profile
if (profile != null) {
val profileManager = SSKEnvironment.shared.profileManager
@@ -865,9 +1438,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey))
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
profileManager.setProfileKey(context, sender, newProfileKey!!)
profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!)
profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN)
profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!)
}
}
threadDB.setHasSent(threadId, true)
@@ -929,16 +1501,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
Optional.absent(),
Optional.absent()
)
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runIncrement = true, runThreadUpdate = true)
mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = true)
}
}
override fun getRecipientApproved(address: Address): Boolean {
return DatabaseComponent.get(context).recipientDatabase().getApproved(address)
}
override fun setRecipientApproved(recipient: Recipient, approved: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved)
if (recipient.isLocalNumber || !recipient.isContactRecipient) return
configFactory.contacts?.upsertContact(recipient.address.serialize()) {
this.approved = approved
}
}
override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
if (recipient.isLocalNumber || !recipient.isContactRecipient) return
configFactory.contacts?.upsertContact(recipient.address.serialize()) {
this.approvedMe = approvedMe
}
}
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
@@ -1076,9 +1660,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms))
}
override fun unblock(toUnblock: Iterable<Recipient>) {
override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
recipientDb.setBlocked(toUnblock, false)
recipientDb.setBlocked(recipients, isBlocked)
recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient ->
configFactory.contacts?.upsertContact(recipient.address.serialize()) {
this.blocked = isBlocked
}
}
val contactsConfig = configFactory.contacts ?: return
if (contactsConfig.needsPush() && !fromConfigUpdate) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
}
override fun blockedContacts(): List<Recipient> {

View File

@@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
@@ -74,6 +73,11 @@ import java.util.Set;
public class ThreadDatabase extends Database {
public interface ConversationThreadUpdateListener {
void threadCreated(@NonNull Address address, long threadId);
void threadDeleted(@NonNull Address address, long threadId);
}
private static final String TAG = ThreadDatabase.class.getSimpleName();
private final Map<Long, Address> addressCache = new HashMap<>();
@@ -141,10 +145,16 @@ public class ThreadDatabase extends Database {
"ADD COLUMN " + UNREAD_MENTION_COUNT + " INTEGER DEFAULT 0;";
}
private ConversationThreadUpdateListener updateListener;
public ThreadDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void setUpdateListener(ConversationThreadUpdateListener updateListener) {
this.updateListener = updateListener;
}
private long createThreadForRecipient(Address address, boolean group, int distributionType) {
ContentValues contentValues = new ContentValues(4);
long date = SnodeAPI.getNowWithOffset();
@@ -207,10 +217,14 @@ public class ThreadDatabase extends Database {
}
private void deleteThread(long threadId) {
Recipient recipient = getRecipientForThreadId(threadId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
int numberRemoved = db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
addressCache.remove(threadId);
notifyConversationListListeners();
if (updateListener != null && numberRemoved > 0 && recipient != null) {
updateListener.threadDeleted(recipient.getAddress(), threadId);
}
}
private void deleteThreads(Set<Long> threadIds) {
@@ -278,7 +292,7 @@ public class ThreadDatabase extends Database {
DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
update(threadId, false);
update(threadId, false, true);
notifyConversationListeners(threadId);
}
} finally {
@@ -291,10 +305,34 @@ public class ThreadDatabase extends Database {
Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp);
DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
update(threadId, false);
update(threadId, false, true);
notifyConversationListeners(threadId);
}
public List<MarkedMessageInfo> setRead(long threadId, long lastReadTime) {
final List<MarkedMessageInfo> smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime);
final List<MarkedMessageInfo> mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime);
if (smsRecords.isEmpty() && mmsRecords.isEmpty()) {
return Collections.emptyList();
}
ContentValues contentValues = new ContentValues(2);
contentValues.put(READ, smsRecords.isEmpty() && mmsRecords.isEmpty());
contentValues.put(LAST_SEEN, lastReadTime);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
notifyConversationListListeners();
return new LinkedList<MarkedMessageInfo>() {{
addAll(smsRecords);
addAll(mmsRecords);
}};
}
public List<MarkedMessageInfo> setRead(long threadId, boolean lastSeen) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);
@@ -319,30 +357,6 @@ public class ThreadDatabase extends Database {
}};
}
public void incrementUnread(long threadId, int amount, int unreadMentionAmount) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
UNREAD_COUNT + " = " + UNREAD_COUNT + " + ?, " +
UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " + ? WHERE " + ID + " = ?",
new String[] {
String.valueOf(amount),
String.valueOf(unreadMentionAmount),
String.valueOf(threadId)
});
}
public void decrementUnread(long threadId, int amount, int unreadMentionAmount) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
UNREAD_COUNT + " = " + UNREAD_COUNT + " - ?, " +
UNREAD_MENTION_COUNT + " = " + UNREAD_MENTION_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0",
new String[] {
String.valueOf(amount),
String.valueOf(unreadMentionAmount),
String.valueOf(threadId)
});
}
public void setDistributionType(long threadId, int distributionType) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(TYPE, distributionType);
@@ -352,6 +366,14 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
}
public void setDate(long threadId, long date) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(DATE, date);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
int updated = db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
if (updated > 0) notifyConversationListListeners();
}
public int getDistributionType(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{TYPE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
@@ -427,9 +449,9 @@ public class ThreadDatabase extends Database {
" ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS +
" LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME +
" ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GROUP_ID +
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " + MESSAGE_COUNT + " = " + UNREAD_COUNT + " AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
" WHERE " + MESSAGE_COUNT + " != 0 AND " + ARCHIVED + " = 0 AND " + HAS_SENT + " = 0 AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.APPROVED + " = 0 AND " +
RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.BLOCK + " = 0 AND " +
GroupDatabase.TABLE_NAME + "." + GROUP_ID + " IS NULL";
cursor = db.rawQuery(query, null);
@@ -481,7 +503,7 @@ public class ThreadDatabase extends Database {
}
public Cursor getApprovedConversationList() {
String where = "((" + MESSAGE_COUNT + " != 0 AND (" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%')) OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
String where = "((" + HAS_SENT + " = 1 OR " + RecipientDatabase.APPROVED + " = 1 OR "+ GroupDatabase.TABLE_NAME +"."+GROUP_ID+" LIKE '"+CLOSED_GROUP_PREFIX+"%') OR " + GroupDatabase.TABLE_NAME + "." + GROUP_ID + " LIKE '" + OPEN_GROUP_PREFIX + "%') " +
"AND " + ARCHIVED + " = 0 ";
return getConversationList(where);
}
@@ -517,21 +539,50 @@ public class ThreadDatabase extends Database {
return db.rawQuery(query, null);
}
public void setLastSeen(long threadId, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
if (timestamp == -1) {
contentValues.put(LAST_SEEN, SnodeAPI.getNowWithOffset());
} else {
contentValues.put(LAST_SEEN, timestamp);
}
/**
* @param threadId
* @param timestamp
* @return true if we have set the last seen for the thread, false if there were no messages in the thread
*/
public boolean setLastSeen(long threadId, long timestamp) {
// edge case where we set the last seen time for a conversation before it loads messages (joining community for example)
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
Recipient forThreadId = getRecipientForThreadId(threadId);
if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && forThreadId != null && forThreadId.isOpenGroupRecipient()) return false;
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
long lastSeenTime = timestamp == -1 ? SnodeAPI.getNowWithOffset() : timestamp;
contentValues.put(LAST_SEEN, lastSeenTime);
db.beginTransaction();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
String smsCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0";
String smsMentionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.READ+" = 0 AND s."+SmsDatabase.HAS_MENTION+" = 1";
String smsReactionCountSubQuery = "SELECT COUNT(*) FROM "+SmsDatabase.TABLE_NAME+" AS s WHERE t."+ID+" = s."+SmsDatabase.THREAD_ID+" AND s."+SmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND s."+SmsDatabase.REACTIONS_UNREAD+" = 1";
String mmsCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0";
String mmsMentionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.READ+" = 0 AND m."+MmsDatabase.HAS_MENTION+" = 1";
String mmsReactionCountSubQuery = "SELECT COUNT(*) FROM "+MmsDatabase.TABLE_NAME+" AS m WHERE t."+ID+" = m."+MmsDatabase.THREAD_ID+" AND m."+MmsDatabase.DATE_SENT+" > t."+LAST_SEEN+" AND m."+MmsDatabase.REACTIONS_UNREAD+" = 1";
String allSmsUnread = "(("+smsCountSubQuery+") + ("+smsReactionCountSubQuery+"))";
String allMmsUnread = "(("+mmsCountSubQuery+") + ("+mmsReactionCountSubQuery+"))";
String allUnread = "(("+allSmsUnread+") + ("+allMmsUnread+"))";
String allUnreadMention = "(("+smsMentionCountSubQuery+") + ("+mmsMentionCountSubQuery+"))";
String reflectUpdates = "UPDATE "+TABLE_NAME+" AS t SET "+UNREAD_COUNT+" = "+allUnread+", "+UNREAD_MENTION_COUNT+" = "+allUnreadMention+" WHERE "+ID+" = ?";
db.execSQL(reflectUpdates, new Object[]{threadId});
db.setTransactionSuccessful();
db.endTransaction();
notifyConversationListeners(threadId);
notifyConversationListListeners();
return true;
}
public void setLastSeen(long threadId) {
setLastSeen(threadId, -1);
/**
* @param threadId
* @return true if we have set the last seen for the thread, false if there were no messages in the thread
*/
public boolean setLastSeen(long threadId) {
return setLastSeen(threadId, -1);
}
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
@@ -634,13 +685,19 @@ public class ThreadDatabase extends Database {
try {
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
long threadId;
boolean created = false;
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
} else {
DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, true);
return createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType);
threadId = createThreadForRecipient(recipient.getAddress(), recipient.isGroupRecipient(), distributionType);
created = true;
}
if (created && updateListener != null) {
updateListener.threadCreated(recipient.getAddress(), threadId);
}
return threadId;
} finally {
if (cursor != null)
cursor.close();
@@ -679,13 +736,14 @@ public class ThreadDatabase extends Database {
new String[] {String.valueOf(threadId)});
notifyConversationListeners(threadId);
notifyConversationListListeners();
}
public boolean update(long threadId, boolean unarchive) {
public boolean update(long threadId, boolean unarchive, boolean shouldDeleteOnEmpty) {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long count = mmsSmsDatabase.getConversationCount(threadId);
boolean shouldDeleteEmptyThread = deleteThreadOnEmpty(threadId);
boolean shouldDeleteEmptyThread = shouldDeleteOnEmpty && deleteThreadOnEmpty(threadId);
if (count == 0 && shouldDeleteEmptyThread) {
deleteThread(threadId);
@@ -708,12 +766,10 @@ public class ThreadDatabase extends Database {
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
notifyConversationListListeners();
return false;
} else {
if (shouldDeleteEmptyThread) {
deleteThread(threadId);
notifyConversationListListeners();
return true;
}
return false;
@@ -721,6 +777,8 @@ public class ThreadDatabase extends Database {
} finally {
if (reader != null)
reader.close();
notifyConversationListListeners();
notifyConversationListeners(threadId);
}
}
@@ -732,10 +790,32 @@ public class ThreadDatabase extends Database {
new String[] {String.valueOf(threadId)});
notifyConversationListeners(threadId);
notifyConversationListListeners();
}
public void markAllAsRead(long threadId, boolean isGroupRecipient) {
List<MarkedMessageInfo> messages = setRead(threadId, true);
public boolean isPinned(long threadId) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{IS_PINNED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getInt(0) == 1;
}
return false;
} finally {
if (cursor != null) cursor.close();
}
}
/**
* @param threadId
* @param isGroupRecipient
* @param lastSeenTime
* @return true if we have set the last seen for the thread, false if there were no messages in the thread
*/
public boolean markAllAsRead(long threadId, boolean isGroupRecipient, long lastSeenTime, boolean force) {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
if (mmsSmsDatabase.getConversationCount(threadId) <= 0 && !force) return false;
List<MarkedMessageInfo> messages = setRead(threadId, lastSeenTime);
if (isGroupRecipient) {
for (MarkedMessageInfo message: messages) {
MarkReadReceiver.scheduleDeletion(context, message.getExpirationInfo());
@@ -743,7 +823,8 @@ public class ThreadDatabase extends Database {
} else {
MarkReadReceiver.process(context, messages);
}
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, false, 0);
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId);
return setLastSeen(threadId, lastSeenTime);
}
private boolean deleteThreadOnEmpty(long threadId) {

View File

@@ -11,7 +11,6 @@ import androidx.core.app.NotificationCompat;
import net.zetetic.database.sqlcipher.SQLiteConnection;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
import net.zetetic.database.sqlcipher.SQLiteException;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
import org.session.libsession.utilities.TextSecurePreferences;
@@ -19,6 +18,7 @@ import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase;
import org.thoughtcrime.securesms.database.ConfigDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase;
@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.database.SessionJobDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities;
import java.io.File;
@@ -86,9 +87,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV38 = 59;
private static final int lokiV39 = 60;
private static final int lokiV40 = 61;
private static final int lokiV41 = 62;
private static final int lokiV42 = 63;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV40;
private static final int DATABASE_VERSION = lokiV42;
private static final int MIN_DATABASE_VERSION = lokiV7;
private static final String CIPHER3_DATABASE_NAME = "signal.db";
public static final String DATABASE_NAME = "signal_v4.db";
@@ -148,7 +151,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
}
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) {
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
@@ -342,6 +345,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(ThreadDatabase.getUnreadMentionCountCommand());
db.execSQL(SmsDatabase.CREATE_HAS_MENTION_COMMAND);
db.execSQL(MmsDatabase.CREATE_HAS_MENTION_COMMAND);
db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
@@ -354,6 +358,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
db.execSQL(RecipientDatabase.getAddWrapperHash());
}
@Override
@@ -587,6 +592,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV41) {
db.execSQL(ConfigDatabase.CREATE_CONFIG_TABLE_COMMAND);
db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_GROUPS);
db.execSQL(ConfigurationMessageUtilities.DELETE_INACTIVE_ONE_TO_ONES);
}
if (oldVersion < lokiV42) {
db.execSQL(RecipientDatabase.getAddWrapperHash());
}
if (oldVersion < lokiV43) {
db.execSQL(RecipientDatabase.getCreateDisappearingStateCommand());
db.execSQL(ExpirationConfigurationDatabase.CREATE_EXPIRATION_CONFIGURATION_TABLE_COMMAND);
db.execSQL(ExpirationConfigurationDatabase.MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_COMMAND);