mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-07 23:12:14 +00:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user