diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 12a76739cf..5a13830bd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.utilities.Address import org.session.libsession.utilities.UploadResult import org.session.libsession.utilities.Util +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceAttachment import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentStream @@ -60,9 +61,9 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return databaseAttachment.toSignalAttachmentPointer() } - override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) { + override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: AttachmentId, messageID: Long) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) - attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value) + attachmentDatabase.setTransferState(messageID, attachmentId, attachmentState.value) } override fun getMessageForQuote(timestamp: Long, author: Address): Pair? { @@ -92,6 +93,14 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return message.linkPreviews.firstOrNull()?.attachmentId?.rowId } + override fun getIndividualRecipientForMms(mmsId: Long): Recipient? { + val mmsDb = DatabaseFactory.getMmsDatabase(context) + val message = mmsDb.getMessage(mmsId).use { + mmsDb.readerFor(it).next + } + return message?.individualRecipient + } + override fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream: InputStream) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream) @@ -110,6 +119,13 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) ), threadId) } + override fun isMmsOutgoing(mmsMessageId: Long): Boolean { + val mmsDb = DatabaseFactory.getMmsDatabase(context) + return mmsDb.getMessage(mmsMessageId).use { cursor -> + mmsDb.readerFor(cursor).next + }.isOutgoing + } + override fun isOutgoingMessage(timestamp: Long): Boolean { val smsDatabase = DatabaseFactory.getSmsDatabase(context) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) 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 c48d7648af..835c35bcb6 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 @@ -90,6 +90,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.* import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase.Drafts @@ -264,6 +265,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe push(intent, false) } + override fun showDialog(baseDialog: BaseDialog, tag: String?) { + baseDialog.show(supportFragmentManager, tag) + } + private fun setUpRecyclerView() { conversationRecyclerView.adapter = adapter val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index accc8b2656..7cd78cda2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -13,6 +13,10 @@ import androidx.core.view.children import androidx.core.view.isVisible import kotlinx.android.synthetic.main.album_thumbnail_view.view.* import network.loki.messenger.R +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.MediaPreviewActivity @@ -23,6 +27,7 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.longmessage.LongMessageActivity import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.video.exo.AttachmentDataSource import kotlin.math.roundToInt class AlbumThumbnailView : FrameLayout { @@ -82,6 +87,13 @@ class AlbumThumbnailView : FrameLayout { // hit intersects with this particular child val slide = slides.getOrNull(index) ?: return // only open to downloaded images + if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + // restart download here + (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> + val attachmentId = attachment.attachmentId.rowId + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId())) + } + } if (slide.isInProgress) return ActivityDispatcher.get(context)?.dispatchIntent { context -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt index fe0423e2e2..db95e49ddd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt @@ -9,6 +9,8 @@ import androidx.appcompat.app.AlertDialog import kotlinx.android.synthetic.main.dialog_download.view.* import network.loki.messenger.R import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.database.DatabaseFactory @@ -36,6 +38,12 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() { } private fun trust() { - // TODO: Implement + val contactDB = DatabaseFactory.getSessionContactDatabase(requireContext()) + val sessionID = recipient.address.toString() + val contact = contactDB.getContactWithSessionID(sessionID) ?: return + val threadID = DatabaseFactory.getThreadDatabase(requireContext()).getThreadIdIfExistsFor(recipient) + contactDB.setContactIsTrusted(contact, true, threadID) + JobQueue.shared.resumePendingJobs(AttachmentDownloadJob.KEY) + dismiss() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt new file mode 100644 index 0000000000..967e080d92 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import kotlinx.android.synthetic.main.view_untrusted_attachment.view.* +import network.loki.messenger.R +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog +import org.thoughtcrime.securesms.util.ActivityDispatcher +import java.util.* + +class UntrustedAttachmentView: LinearLayout { + + enum class AttachmentType { + AUDIO, + DOCUMENT, + MEDIA + } + + // region Lifecycle + constructor(context: Context) : super(context) { initialize() } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } + + private fun initialize() { + LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this) + } + // endregion + + // region Updating + fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int) { + val (iconRes, stringRes) = when (attachmentType) { + AttachmentType.AUDIO -> R.drawable.ic_microphone to R.string.Slide_audio + AttachmentType.DOCUMENT -> R.drawable.ic_document_large_light to R.string.document + AttachmentType.MEDIA -> R.drawable.ic_image_white_24dp to R.string.media + } + val iconDrawable = ContextCompat.getDrawable(context,iconRes)!! + iconDrawable.mutate().setTint(textColor) + val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT)) + + untrustedAttachmentIcon.setImageDrawable(iconDrawable) + untrustedAttachmentTitle.text = text + } + // endregion + + // region Interaction + fun showTrustDialog(recipient: Recipient) { + ActivityDispatcher.get(context)?.showDialog(DownloadDialog(recipient)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 1750d8bb0b..1d90f8475a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -63,7 +63,7 @@ class VisibleMessageContentView : LinearLayout { // region Updating fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean, - glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?) { + glide: GlideRequests, maxWidth: Int, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) { // Background val background = getBackground(message.isOutgoing, isStartOfMessageCluster, isEndOfMessageCluster) val colorID = if (message.isOutgoing) R.attr.message_sent_background_color else R.attr.message_received_background_color @@ -108,32 +108,56 @@ class VisibleMessageContentView : LinearLayout { } } } else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) { - val voiceMessageView = VoiceMessageView(context) - voiceMessageView.index = viewHolderIndex - voiceMessageView.delegate = context as? ConversationActivityV2 - voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) - mainContainer.addView(voiceMessageView) - // We have to use onContentClick (rather than a click listener directly on the voice - // message view) so as to not interfere with all the other gestures. - onContentClick = { voiceMessageView.togglePlayback() } - onContentDoubleTap = { voiceMessageView.handleDoubleTap() } + // Audio attachment + if (contactIsTrusted || message.isOutgoing) { + val voiceMessageView = VoiceMessageView(context) + voiceMessageView.index = viewHolderIndex + voiceMessageView.delegate = context as? ConversationActivityV2 + voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) + mainContainer.addView(voiceMessageView) + // We have to use onContentClick (rather than a click listener directly on the voice + // message view) so as to not interfere with all the other gestures. + onContentClick = { voiceMessageView.togglePlayback() } + onContentDoubleTap = { voiceMessageView.handleDoubleTap() } + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } + } } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { - val documentView = DocumentView(context) - documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) - mainContainer.addView(documentView) + // Document attachment + if (contactIsTrusted || message.isOutgoing) { + val documentView = DocumentView(context) + documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) + mainContainer.addView(documentView) + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } + } } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { - val albumThumbnailView = AlbumThumbnailView(context) - mainContainer.addView(albumThumbnailView) - // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups - // bind after add view because views are inflated and calculated during bind - albumThumbnailView.bind( - glideRequests = glide, - message = message, - isStart = isStartOfMessageCluster, - isEnd = isEndOfMessageCluster - ) - onContentClick = { event -> - albumThumbnailView.calculateHitObject(event, message, thread) + // Images/Video attachment + if (contactIsTrusted || message.isOutgoing) { + val albumThumbnailView = AlbumThumbnailView(context) + mainContainer.addView(albumThumbnailView) + // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups + // bind after add view because views are inflated and calculated during bind + albumThumbnailView.bind( + glideRequests = glide, + message = message, + isStart = isStartOfMessageCluster, + isEnd = isEndOfMessageCluster + ) + onContentClick = { event -> + albumThumbnailView.calculateHitObject(event, message, thread) + } + } else { + val untrustedView = UntrustedAttachmentView(context) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) + mainContainer.addView(untrustedView) + onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } } } else if (message.isOpenGroupInvitation) { val openGroupInvitationView = OpenGroupInvitationView(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index bfd7fa2cb1..0fcbf685b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -81,6 +81,7 @@ class VisibleMessageView : LinearLayout { val threadDB = DatabaseFactory.getThreadDatabase(context) val thread = threadDB.getRecipientForThreadId(threadID) ?: return val contactDB = DatabaseFactory.getSessionContactDatabase(context) + val contact = contactDB.getContactWithSessionID(senderSessionID) val isGroupThread = thread.isGroupRecipient val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) @@ -100,7 +101,7 @@ class VisibleMessageView : LinearLayout { } senderNameTextView.isVisible = isStartOfMessageCluster val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR - senderNameTextView.text = contactDB.getContactWithSessionID(senderSessionID)?.displayName(context) ?: senderSessionID + senderNameTextView.text = contact?.displayName(context) ?: senderSessionID } else { profilePictureContainer.visibility = View.GONE senderNameTextView.visibility = View.GONE @@ -149,7 +150,7 @@ class VisibleMessageView : LinearLayout { if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } // Populate content view messageContentView.viewHolderIndex = viewHolderIndex - messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery) + messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) messageContentView.delegate = contentViewDelegate onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 7823cc59d1..6498a1a472 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -31,7 +31,6 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { private var progress = 0.0 private var duration = 0L private var player: AudioSlidePlayer? = null - private var isPreparing = false var delegate: VoiceMessageViewDelegate? = null var index = -1 @@ -51,7 +50,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { // region Updating fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { val audio = message.slideDeck.audioSlide!! - voiceMessageViewLoader.isVisible = audio.isPendingDownload + voiceMessageViewLoader.isVisible = audio.isInProgress val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopRightRadius(cornerRadii[1]) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt index d432ed0f79..80b233c42d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt @@ -40,6 +40,7 @@ open class KThumbnailView: FrameLayout { private val image by lazy { thumbnail_image } private val playOverlay by lazy { play_overlay } val loadIndicator: View by lazy { thumbnail_load_indicator } + val downloadIndicator: View by lazy { thumbnail_download_icon } private val dimensDelegate = ThumbnailDimensDelegate() @@ -108,7 +109,8 @@ open class KThumbnailView: FrameLayout { this.slide = slide - loadIndicator.isVisible = slide.isInProgress && !mms.isFailed + loadIndicator.isVisible = slide.isInProgress + downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED dimensDelegate.setDimens(naturalWidth, naturalHeight) invalidate() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index 0c3159ee16..9bcf94ec1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -45,6 +45,17 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da }.toSet() } + fun setContactIsTrusted(contact: Contact, isTrusted: Boolean, threadID: Long) { + val database = databaseHelper.writableDatabase + val contentValues = ContentValues(1) + contentValues.put(Companion.isTrusted, if (isTrusted) 1 else 0) + database.update(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID )) + if (threadID >= 0) { + notifyConversationListeners(threadID) + } + notifyConversationListListeners() + } + fun setContact(contact: Contact) { val database = databaseHelper.writableDatabase val contentValues = ContentValues(8) @@ -56,7 +67,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da contact.profilePictureEncryptionKey?.let { contentValues.put(profilePictureEncryptionKey, Base64.encodeBytes(it)) } - contentValues.put(threadID, threadID) + contentValues.put(threadID, contact.threadID) contentValues.put(isTrusted, if (contact.isTrusted) 1 else 0) database.insertOrUpdate(sessionContactTable, contentValues, "$sessionID = ?", arrayOf( contact.sessionID )) notifyConversationListListeners() 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 ee33b2dfcb..6dec2c421c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -503,9 +503,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val mmsDb = DatabaseFactory.getMmsDatabase(context) val cursor = mmsDb.getMessage(mmsId) val reader = mmsDb.readerFor(cursor) - val threadId = reader.next.threadId + val threadId = reader.next?.threadId cursor.close() - return threadId + return threadId ?: -1 } override fun getContactWithSessionID(sessionID: String): Contact? { @@ -520,6 +520,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getSessionContactDatabase(context).setContact(contact) } + override fun getRecipientForThread(threadId: Long): Recipient? { + return DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) + } + override fun getRecipientSettings(address: Address): Recipient.RecipientSettings? { val recipientSettings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address) return if (recipientSettings.isPresent) { recipientSettings.get() } else null diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index 132cccf072..d5daba5872 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog fun BaseActionBarActivity.setUpActionBarSessionLogo(hideBackButton: Boolean = false) { val actionbar = supportActionBar!! @@ -65,4 +66,5 @@ interface ActivityDispatcher { fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher } fun dispatchIntent(body: (Context)->Intent?) + fun showDialog(baseDialog: BaseDialog, tag: String? = null) } \ No newline at end of file diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index 8922112823..eaa98cce65 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -22,6 +22,15 @@ android:visibility="gone" tools:visibility="visible" /> + + + android:visibility="gone"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d31dbcf6e..1f11b6e3f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -874,4 +874,6 @@ %s is blocked. Unblock them? Failed to prepare attachment for sending. + Media + Tap to download %s diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index 39ca4854ce..94f59c4b43 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -3,6 +3,7 @@ package org.session.libsession.database import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.utilities.Address import org.session.libsession.utilities.UploadResult +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentStream import java.io.InputStream @@ -18,10 +19,11 @@ interface MessageDataProvider { fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? - fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) + fun setAttachmentState(attachmentState: AttachmentState, attachmentId: AttachmentId, messageID: Long) fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream) fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long) - fun isOutgoingMessage(timestamp: Long): Boolean + fun isMmsOutgoing(mmsMessageId: Long): Boolean + fun isOutgoingMessage(mmsId: Long): Boolean fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleFailedAttachmentUpload(attachmentId: Long) fun getMessageForQuote(timestamp: Long, author: Address): Pair? @@ -29,4 +31,5 @@ interface MessageDataProvider { fun getMessageBodyFor(timestamp: Long, author: String): String fun getAttachmentIDsFor(messageID: Long): List fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? + fun getIndividualRecipientForMms(mmsId: Long): Recipient? } \ No newline at end of file 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 8a21e61513..9e3ac258b9 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -139,6 +139,7 @@ interface StorageProtocol { fun getContactWithSessionID(sessionID: String): Contact? fun getAllContacts(): Set fun setContact(contact: Contact) + fun getRecipientForThread(threadId: Long): Recipient? fun getRecipientSettings(address: Address): RecipientSettings? fun addContacts(contacts: List) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index d0a09dc771..6df64c2d80 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.utilities.Data @@ -16,6 +17,7 @@ import org.session.libsignal.utilities.Log import java.io.File import java.io.FileInputStream import java.io.InputStream +import java.lang.NullPointerException class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job { override var delegate: JobDelegate? = null @@ -25,6 +27,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) // Error internal sealed class Error(val description: String) : Exception(description) { object NoAttachment : Error("No such attachment.") + object NoThread: Error("Thread no longer exists") + object NoSender: Error("Thread recipient or sender does not exist") + object DuplicateData: Error("Attachment already downloaded") } // Settings @@ -41,22 +46,56 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) override fun execute() { val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider - val handleFailure: (java.lang.Exception) -> Unit = { exception -> + val threadID = storage.getThreadIdForMms(databaseMessageID) + + val handleFailure: (java.lang.Exception, attachmentId: AttachmentId?) -> Unit = { exception, attachment -> if (exception == Error.NoAttachment + || exception == Error.NoThread + || exception == Error.NoSender || (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) { - messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) + attachment?.let { id -> + messageDataProvider.setAttachmentState(AttachmentState.FAILED, id, databaseMessageID) + } ?: run { + messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID) + } this.handlePermanentFailure(exception) } else { this.handleFailure(exception) } } + + if (threadID < 0) { + handleFailure(Error.NoThread, null) + return + } + + val threadRecipient = storage.getRecipientForThread(threadID) + val sender = if (messageDataProvider.isMmsOutgoing(databaseMessageID)) { + storage.getUserPublicKey() + } else { + messageDataProvider.getIndividualRecipientForMms(databaseMessageID)?.address?.serialize() + } + val contact = sender?.let { storage.getContactWithSessionID(it) } + if (threadRecipient == null || sender == null || contact == null) { + handleFailure(Error.NoSender, null) + return + } + if (!threadRecipient.isGroupRecipient && (!contact.isTrusted && storage.getUserPublicKey() != sender)) { + // if we aren't receiving a group message, a message from ourselves (self-send) and the contact sending is not trusted: + // do not continue, but do not fail + return + } + var tempFile: File? = null try { val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) - ?: return handleFailure(Error.NoAttachment) - messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) + ?: return handleFailure(Error.NoAttachment, null) + if (attachment.hasData()) { + handleFailure(Error.DuplicateData, attachment.attachmentId) + return + } + messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachment.attachmentId, this.databaseMessageID) tempFile = createTempFile() - val threadID = storage.getThreadIdForMms(databaseMessageID) val openGroupV2 = storage.getV2OpenGroup(threadID) if (openGroupV2 == null) { DownloadUtilities.downloadFile(tempFile, attachment.url) @@ -89,7 +128,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) handleSuccess() } catch (e: Exception) { tempFile?.delete() - return handleFailure(e) + return handleFailure(e,null) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 9ced85b110..205db3d6f8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -20,7 +20,7 @@ class JobQueue : JobDelegate { private val jobTimestampMap = ConcurrentHashMap() private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() + private val attachmentDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() private val scope = GlobalScope + SupervisorJob() private val queue = Channel(UNLIMITED) private val pendingJobIds = mutableSetOf() @@ -100,6 +100,23 @@ class JobQueue : JobDelegate { Log.d("Loki", "resumed pending send message $id") } + fun resumePendingJobs(typeKey: String) { + val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(typeKey) + val pendingJobs = mutableListOf() + for ((id, job) in allPendingJobs) { + if (job == null) { + // Job failed to deserialize, remove it from the DB + handleJobFailedPermanently(id) + } else { + pendingJobs.add(job) + } + } + pendingJobs.sortedBy { it.id }.forEach { job -> + Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.") + queue.offer(job) // Offer always called on unlimited capacity + } + } + fun resumePendingJobs() { if (hasResumedPendingJobs) { Log.d("Loki", "resumePendingJobs() should only be called once.") @@ -114,20 +131,7 @@ class JobQueue : JobDelegate { NotifyPNServerJob.KEY ) allJobTypes.forEach { type -> - val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type) - val pendingJobs = mutableListOf() - for ((id, job) in allPendingJobs) { - if (job == null) { - // Job failed to deserialize, remove it from the DB - handleJobFailedPermanently(id) - } else { - pendingJobs.add(job) - } - } - pendingJobs.sortedBy { it.id }.forEach { job -> - Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.") - queue.offer(job) // Offer always called on unlimited capacity - } + resumePendingJobs(type) } }