diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 8b9d7340c2..ba30937004 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -3,22 +3,34 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface import android.net.Uri -import android.os.* +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.provider.MediaStore -import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.SpannedString import android.text.TextUtils import android.text.style.StyleSpan import android.util.Pair import android.util.TypedValue -import android.view.* +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.Toast @@ -72,8 +84,12 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.snode.SnodeAPI -import org.session.libsession.utilities.* +import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.Stub +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientModifiedListener @@ -106,10 +122,25 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel -import org.thoughtcrime.securesms.conversation.v2.utilities.* +import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.database.* +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.LokiAPIDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ReactionDatabase +import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -122,13 +153,26 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity -import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.mms.AudioSlide +import org.thoughtcrime.securesms.mms.GifSlide +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.mms.MediaConstraints +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment -import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.toPx import java.lang.ref.WeakReference -import java.util.* +import java.util.Locale import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference @@ -455,6 +499,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // TODO: buffer this bufferedLastSeenChannel.trySend(Unit) } + updatePlaceholder() } override fun onLoaderReset(cursor: Loader) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index e460574b1c..2498e90b69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -154,18 +154,14 @@ public class RecipientDatabase extends Database { public Optional 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(); } } @@ -252,6 +248,16 @@ 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 setApproved(@NonNull Recipient recipient, boolean approved) { ContentValues values = new ContentValues(); values.put(APPROVED, approved ? 1 : 0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 0a8aae2d59..18309249c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -98,6 +98,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF // TODO: maybe add time here from formation / creation message override fun threadCreated(address: Address, threadId: Long) { + if (!getRecipientApproved(address)) return // don't store unapproved / message requests Log.d("Loki-DBG", "creating thread for $address\nExecution context:\n${Thread.currentThread().stackTrace.joinToString("\n")}") val volatile = configFactory.convoVolatile ?: return @@ -1037,6 +1038,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF override fun setContact(contact: Contact) { DatabaseComponent.get(context).sessionContactDatabase().setContact(contact) + if (!getRecipientApproved(Address.fromSerialized(contact.sessionID))) return SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact) } @@ -1050,6 +1052,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF } override fun addLibSessionContacts(contacts: List) { + Log.d("Loki-DBG", "Adding contacts from execution context:\n${Thread.currentThread().stackTrace.joinToString("\n")}") val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val moreContacts = contacts.filter { contact -> val id = SessionId(contact.id) @@ -1197,7 +1200,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF Log.d("Loki-DBG", "Deleting conversation for ${recipient.address}") if (recipient.isContactRecipient) { if (recipient.isLocalNumber) return - // TODO: handle contact val contacts = configFactory.contacts ?: return contacts.upsertContact(recipient.address.serialize()) { this.priority = ConfigBase.PRIORITY_HIDDEN @@ -1351,6 +1353,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF } } + 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 diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index dbdbb75a46..75aebab58b 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -193,6 +193,7 @@ interface StorageProtocol { fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertMessageRequestResponse(response: MessageRequestResponse) fun setRecipientApproved(recipient: Recipient, approved: Boolean) + fun getRecipientApproved(address: Address): Boolean fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) fun conversationHasOutgoing(userPublicKey: String): Boolean diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 530cacc832..aeb31ff209 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -10,8 +10,15 @@ import nl.komponents.kovenant.task import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage +import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.MessageRequestResponse +import org.session.libsession.messaging.messages.control.ReadReceipt +import org.session.libsession.messaging.messages.control.SharedConfigurationMessage +import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -54,6 +61,9 @@ class BatchMessageReceiveJob( const val BATCH_DEFAULT_NUMBER = 512 + // used for processing messages that don't have a thread and shouldn't create one + const val NO_THREAD_MAPPING = -1L + // Keys used for database storage private val NUM_MESSAGES_KEY = "numMessages" private val DATA_KEY = "data" @@ -64,18 +74,22 @@ class BatchMessageReceiveJob( private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean { val message = parsedMessage.message - return message is VisibleMessage - || !message.isSenderSelf - // TODO: sort out which messages should create threads: message requests? group creation threads? visible messages? others? calls? - -// || (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) // this was creating threads for self send messages (i.e. synced group creation) -// if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) { -// return true -// } -// if (parsedMessage.message.isSenderSelf ) { -// // all the cases where we should add a self send creating the thread, i.e. group invite? visible message? -// } -// !parsedMessage.message.isSenderSelf || parsedMessage.message is VisibleMessage + if (message is VisibleMessage) return true + else { // message is control message otherwise + return when(message) { + is SharedConfigurationMessage -> false + is ClosedGroupControlMessage -> message.kind is ClosedGroupControlMessage.Kind.New + is DataExtractionNotification -> false + is MessageRequestResponse -> false + is ExpirationTimerUpdate -> false + is ConfigurationMessage -> false + is TypingIndicator -> false + is UnsendRequest -> false + is ReadReceipt -> false + is CallMessage -> false // TODO: maybe + else -> false // shouldn't happen, or I guess would be Visible + } + } } private fun getThreadId(message: Message, storage: StorageProtocol, shouldCreateThread: Boolean): Long? { @@ -106,8 +120,8 @@ class BatchMessageReceiveJob( val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey) message.serverHash = serverHash val parsedParams = ParsedMessage(messageParameters, message, proto) - val threadID = getThreadId(message, storage, shouldCreateThread(parsedParams)) - if (threadID != null && !threadMap.containsKey(threadID)) { + val threadID = getThreadId(message, storage, shouldCreateThread(parsedParams)) ?: NO_THREAD_MAPPING + if (!threadMap.containsKey(threadID)) { threadMap[threadID] = mutableListOf(parsedParams) } else { threadMap[threadID]!! += parsedParams @@ -136,76 +150,97 @@ class BatchMessageReceiveJob( // iterate over threads and persist them (persistence is the longest constant in the batch process operation) runBlocking(Dispatchers.IO) { - val deferredThreadMap = threadMap.entries.map { (threadId, messages) -> - async { - // The LinkedHashMap should preserve insertion order - val messageIds = linkedMapOf>() - val myLastSeen = storage.getLastSeen(threadId) - var newLastSeen = myLastSeen - messages.forEach { (parameters, message, proto) -> - try { - when (message) { - is VisibleMessage -> { - val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId( - IdPrefix.BLINDED, it.publicKey.asBytes).hexString } - val sentTimestamp = message.sentTimestamp!! - if (message.sender == localUserPublicKey || isUserBlindedSender) { - if (sentTimestamp > newLastSeen) { - newLastSeen = sentTimestamp // use sent timestamp here since that is technically the last one we have - } - } - val messageId = MessageReceiver.handleVisibleMessage( - message, proto, openGroupID, - runThreadUpdate = false, - runProfileUpdate = true - ) - if (messageId != null && message.reaction == null) { - messageIds[messageId] = Pair( - (message.sender == localUserPublicKey || isUserBlindedSender), - message.hasMention + fun processMessages(threadId: Long, messages: List) = async { + // The LinkedHashMap should preserve insertion order + val messageIds = linkedMapOf>() + val myLastSeen = storage.getLastSeen(threadId) + var newLastSeen = myLastSeen + messages.forEach { (parameters, message, proto) -> + try { + when (message) { + is VisibleMessage -> { + val isUserBlindedSender = + message.sender == serverPublicKey?.let { + SodiumUtilities.blindedKeyPair( + it, + MessagingModuleConfiguration.shared.getUserED25519KeyPair()!! ) + }?.let { + SessionId( + IdPrefix.BLINDED, it.publicKey.asBytes + ).hexString } - parameters.openGroupMessageServerID?.let { - MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions) + val sentTimestamp = message.sentTimestamp!! + if (message.sender == localUserPublicKey || isUserBlindedSender) { + if (sentTimestamp > newLastSeen) { + newLastSeen = + sentTimestamp // use sent timestamp here since that is technically the last one we have } } + val messageId = MessageReceiver.handleVisibleMessage( + message, proto, openGroupID, + runThreadUpdate = false, + runProfileUpdate = true + ) - is UnsendRequest -> { - val deletedMessageId = MessageReceiver.handleUnsendRequest(message) - - // If we removed a message then ensure it isn't in the 'messageIds' - if (deletedMessageId != null) { - messageIds.remove(deletedMessageId) - } + if (messageId != null && message.reaction == null) { + messageIds[messageId] = Pair( + (message.sender == localUserPublicKey || isUserBlindedSender), + message.hasMention + ) } + parameters.openGroupMessageServerID?.let { + MessageReceiver.handleOpenGroupReactions( + threadId, + it, + parameters.reactions + ) + } + } - else -> MessageReceiver.handle(message, proto, openGroupID) - } - } catch (e: Exception) { - Log.e(TAG, "Couldn't process message (id: $id)", e) - if (e is MessageReceiver.Error && !e.isRetryable) { - Log.e(TAG, "Message failed permanently (id: $id)", e) - } else { - Log.e(TAG, "Message failed (id: $id)", e) - failures += parameters + is UnsendRequest -> { + val deletedMessageId = + MessageReceiver.handleUnsendRequest(message) + + // If we removed a message then ensure it isn't in the 'messageIds' + if (deletedMessageId != null) { + messageIds.remove(deletedMessageId) + } } + + else -> MessageReceiver.handle(message, proto, openGroupID) + } + } catch (e: Exception) { + Log.e(TAG, "Couldn't process message (id: $id)", e) + if (e is MessageReceiver.Error && !e.isRetryable) { + Log.e(TAG, "Message failed permanently (id: $id)", e) + } else { + Log.e(TAG, "Message failed (id: $id)", e) + failures += parameters } } - // increment unreads, notify, and update thread - // last seen will be the current last seen if not changed (re-computes the read counts for thread record) - // might have been updated from a different thread at this point - val currentLastSeen = storage.getLastSeen(threadId) - if (currentLastSeen > newLastSeen) { - newLastSeen = currentLastSeen - } - storage.markConversationAsRead(threadId, newLastSeen) - storage.updateThread(threadId, true) - SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) } + // increment unreads, notify, and update thread + // last seen will be the current last seen if not changed (re-computes the read counts for thread record) + // might have been updated from a different thread at this point + val currentLastSeen = storage.getLastSeen(threadId) + if (currentLastSeen > newLastSeen) { + newLastSeen = currentLastSeen + } + storage.markConversationAsRead(threadId, newLastSeen) + storage.updateThread(threadId, true) + SSKEnvironment.shared.notificationManager.updateNotification(context, threadId) + } + + val withoutDefault = threadMap.entries.filter { it.key != NO_THREAD_MAPPING } + val noThreadMessages = threadMap[NO_THREAD_MAPPING] ?: listOf() + val deferredThreadMap = withoutDefault.map { (threadId, messages) -> + processMessages(threadId, messages) } // await all thread processing deferredThreadMap.awaitAll() + processMessages(NO_THREAD_MAPPING, noThreadMessages) } if (failures.isEmpty()) { handleSuccess(dispatcherName) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index ff85d3211c..e6fb7b434b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -11,7 +11,7 @@ import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.Log import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment -class VisibleMessage : Message() { +class VisibleMessage : Message() { /** In the case of a sync message, the public key of the person the message was targeted at. * * **Note:** `nil` if this isn't a sync message.