This commit is contained in:
Anton Chekulaev 2020-12-18 13:42:03 +11:00
commit dbd662d6d2
11 changed files with 352 additions and 114 deletions

View File

@ -2,27 +2,35 @@ package org.thoughtcrime.securesms.attachments
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.database.dto.DatabaseAttachmentDTO
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.dto.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
import org.session.libsignal.libsignal.util.guava.Optional
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider { class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider {
override fun getAttachment(attachmentId: Long): DatabaseAttachmentDTO? {
override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
return databaseAttachment.toDTO() return databaseAttachment.toAttachmentStream(context)
} }
override fun setAttachmentState(attachmentState: AttachmentState, attachment: DatabaseAttachmentDTO, messageID: Long) { override fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer? {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
attachmentDatabase.setTransferState(messageID, AttachmentId(attachment.attachmentId, 0), attachmentState.value) val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
return databaseAttachment.toAttachmentPointer()
}
override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -38,29 +46,26 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
} }
// Extension to DatabaseAttachment class fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
return SessionServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
fun DatabaseAttachment.toDTO(): DatabaseAttachmentDTO {
var databaseAttachmentDTO = DatabaseAttachmentDTO()
databaseAttachmentDTO.attachmentId = this.attachmentId.rowId
databaseAttachmentDTO.contentType = this.contentType
databaseAttachmentDTO.fileName = this.fileName
databaseAttachmentDTO.caption = this.caption
databaseAttachmentDTO.size = this.size.toInt()
databaseAttachmentDTO.key = ByteString.copyFrom(this.key?.toByteArray())
databaseAttachmentDTO.digest = ByteString.copyFrom(this.digest)
databaseAttachmentDTO.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
databaseAttachmentDTO.url = this.url
if (this.shouldHaveImageSize()) {
databaseAttachmentDTO.shouldHaveImageSize = true
databaseAttachmentDTO.width = this.width
databaseAttachmentDTO.height = this.height
} }
return databaseAttachmentDTO fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttachmentStream {
val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!)
var attachmentStream = SessionServiceAttachmentStream(stream, this.contentType, this.size, Optional.fromNullable(this.fileName), this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption), null)
attachmentStream.attachmentId = this.attachmentId.rowId
attachmentStream.isAudio = MediaUtil.isAudio(this)
attachmentStream.isGif = MediaUtil.isGif(this)
attachmentStream.isVideo = MediaUtil.isVideo(this)
attachmentStream.isImage = MediaUtil.isImage(this)
attachmentStream.key = ByteString.copyFrom(this.key?.toByteArray())
attachmentStream.digest = this.digest
//attachmentStream.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
attachmentStream.url = this.url
return attachmentStream
} }
fun DatabaseAttachment.shouldHaveImageSize(): Boolean { fun DatabaseAttachment.shouldHaveImageSize(): Boolean {

View File

@ -1,14 +1,18 @@
package org.session.libsession.database package org.session.libsession.database
import org.session.libsession.database.dto.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.database.dto.DatabaseAttachmentDTO import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
interface MessageDataProvider { interface MessageDataProvider {
fun getAttachment(attachmentId: Long): DatabaseAttachmentDTO? //fun getAttachment(attachmentId: Long): SignalServiceAttachmentStream?
fun setAttachmentState(attachmentState: AttachmentState, attachment: DatabaseAttachmentDTO, messageID: Long) fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
fun isOutgoingMessage(timestamp: Long): Boolean fun isOutgoingMessage(timestamp: Long): Boolean

View File

@ -1,75 +0,0 @@
package org.session.libsession.database.dto
import android.util.Size
import com.google.protobuf.ByteString
import org.session.libsignal.service.internal.push.SignalServiceProtos
import kotlin.math.round
class DatabaseAttachmentDTO {
var attachmentId: Long = 0
var contentType: String? = null
var fileName: String? = null
var url: String? = null
var caption: String? = null
var size: Int = 0
var key: ByteString? = null
var digest: ByteString? = null
var flags: Int = 0
var width: Int = 0
var height: Int = 0
val isVoiceNote: Boolean = false
var shouldHaveImageSize: Boolean = false
val isUploaded: Boolean = false
fun toProto(): SignalServiceProtos.AttachmentPointer? {
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
builder.contentType = this.contentType
if (!this.fileName.isNullOrEmpty()) {
builder.fileName = this.fileName
}
if (!this.caption.isNullOrEmpty()) {
builder.caption = this.caption
}
builder.size = this.size
builder.key = this.key
builder.digest = this.digest
builder.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
if (this.shouldHaveImageSize) {
if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) {
val imageSize= Size(this.width, this.height)
val imageWidth = round(imageSize.width.toDouble())
val imageHeight = round(imageSize.height.toDouble())
if (imageWidth > 0 && imageHeight > 0) {
builder.width = imageWidth.toInt()
builder.height = imageHeight.toInt()
}
}
}
builder.url = this.url
try {
return builder.build()
} catch (e: Exception) {
return null
}
}
}

View File

@ -1,17 +1,94 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
class AttachmentUploadJob : Job { import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.libsignal.logging.Log
import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
override var delegate: JobDelegate? = null override var delegate: JobDelegate? = null
override var id: String? = null override var id: String? = null
override var failureCount: Int = 0 override var failureCount: Int = 0
// Error
internal sealed class Error(val description: String) : Exception() {
object NoAttachment : Error("No such attachment.")
}
// Settings // Settings
override val maxFailureCount: Int = 20 override val maxFailureCount: Int = 20
companion object { companion object {
val TAG = AttachmentUploadJob::class.qualifiedName
val collection: String = "AttachmentUploadJobCollection" val collection: String = "AttachmentUploadJobCollection"
val maxFailureCount: Int = 20
} }
override fun execute() { override fun execute() {
TODO("Not yet implemented") try {
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
val server = openGroup?.server ?: FileServerAPI.server
//TODO add some encryption stuff here
val isEncryptionRequired = false
//val isEncryptionRequired = (server == FileServerAPI.server)
val attachmentKey = Util.getSecretBytes(64)
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val ciphertextLength = attachmentStream.length
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
FileServerAPI.shared.uploadAttachment(server, attachmentData)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
} else {
this.handleFailure(e)
}
}
}
private fun handleSuccess() {
Log.w(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this)
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
//TODO interaction stuff, not sure how to deal with that
}
private fun handlePermanentFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e)
failAssociatedMessageSendJob(e)
}
private fun handleFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed due to error: $this.")
delegate?.handleJobFailed(this, e)
if (failureCount + 1 == AttachmentUploadJob.maxFailureCount) {
failAssociatedMessageSendJob(e)
}
}
private fun failAssociatedMessageSendJob(e: Exception) {
val storage = MessagingConfiguration.shared.storage
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message!!, e)
if (messageSendJob != null) {
storage.markJobAsFailed(messageSendJob)
}
} }
} }

View File

@ -27,7 +27,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val message = message as? VisibleMessage val message = message as? VisibleMessage
message?.let { message?.let {
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachment(it) }.filterNotNull() val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
val attachmentsToUpload = attachments.filter { !it.isUploaded } val attachmentsToUpload = attachments.filter { !it.isUploaded }
attachmentsToUpload.forEach { attachmentsToUpload.forEach {
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) { if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {

View File

@ -44,7 +44,7 @@ class LinkPreview() {
title?.let { linkPreviewProto.title = title } title?.let { linkPreviewProto.title = title }
val attachmentID = attachmentID val attachmentID = attachmentID
attachmentID?.let { attachmentID?.let {
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachment(attachmentID) val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() } attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
} }
// Build // Build

View File

@ -60,7 +60,7 @@ class Quote() {
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, messageDataProvider: MessageDataProvider) { private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, messageDataProvider: MessageDataProvider) {
val attachmentID = attachmentID ?: return val attachmentID = attachmentID ?: return
val attachmentProto = messageDataProvider.getAttachment(attachmentID) val attachmentProto = messageDataProvider.getAttachmentStream(attachmentID)
if (attachmentProto == null) { if (attachmentProto == null) {
Log.w(TAG, "Ignoring invalid attachment for quoted message.") Log.w(TAG, "Ignoring invalid attachment for quoted message.")
return return
@ -74,7 +74,7 @@ class Quote() {
} }
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
quotedAttachmentProto.contentType = attachmentProto.contentType quotedAttachmentProto.contentType = attachmentProto.contentType
val fileName = attachmentProto.fileName val fileName = attachmentProto.fileName?.get()
fileName?.let { quotedAttachmentProto.fileName = fileName } fileName?.let { quotedAttachmentProto.fileName = fileName }
quotedAttachmentProto.thumbnail = attachmentProto.toProto() quotedAttachmentProto.thumbnail = attachmentProto.toProto()
try { try {

View File

@ -90,7 +90,7 @@ class VisibleMessage : Message() {
} }
} }
//Attachments //Attachments
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachment(it) } val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(it) }
if (!attachments.all { it.isUploaded }) { if (!attachments.all { it.isUploaded }) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//TODO equivalent to iOS's preconditionFailure //TODO equivalent to iOS's preconditionFailure

View File

@ -0,0 +1,123 @@
package org.session.libsession.messaging.sending_receiving.attachments
import com.google.protobuf.ByteString
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceAttachment
import java.io.InputStream
abstract class SessionServiceAttachment protected constructor(val contentType: String?) {
var attachmentId: Long = 0
var isGif: Boolean = false
var isImage: Boolean = false
var isVideo: Boolean = false
var isAudio: Boolean = false
var url: String = ""
var key: ByteString? = null
abstract fun isStream(): Boolean
abstract fun isPointer(): Boolean
fun asStream(): SessionServiceAttachmentStream {
return this as SessionServiceAttachmentStream
}
fun asPointer(): SessionServiceAttachmentPointer {
return this as SessionServiceAttachmentPointer
}
fun shouldHaveImageSize(): Boolean {
return (isVideo || isImage || isGif);
}
class Builder internal constructor() {
private var inputStream: InputStream? = null
private var contentType: String? = null
private var fileName: String? = null
private var length: Long = 0
private var listener: SignalServiceAttachment.ProgressListener? = null
private var voiceNote = false
private var width = 0
private var height = 0
private var caption: String? = null
fun withStream(inputStream: InputStream?): Builder {
this.inputStream = inputStream
return this
}
fun withContentType(contentType: String?): Builder {
this.contentType = contentType
return this
}
fun withLength(length: Long): Builder {
this.length = length
return this
}
fun withFileName(fileName: String?): Builder {
this.fileName = fileName
return this
}
fun withListener(listener: SignalServiceAttachment.ProgressListener?): Builder {
this.listener = listener
return this
}
fun withVoiceNote(voiceNote: Boolean): Builder {
this.voiceNote = voiceNote
return this
}
fun withWidth(width: Int): Builder {
this.width = width
return this
}
fun withHeight(height: Int): Builder {
this.height = height
return this
}
fun withCaption(caption: String?): Builder {
this.caption = caption
return this
}
fun build(): SessionServiceAttachmentStream {
requireNotNull(inputStream) { "Must specify stream!" }
requireNotNull(contentType) { "No content type specified!" }
require(length != 0L) { "No length specified!" }
return SessionServiceAttachmentStream(inputStream, contentType, length, Optional.fromNullable(fileName), voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption), listener)
}
}
/**
* An interface to receive progress information on upload/download of
* an attachment.
*/
/*interface ProgressListener {
/**
* Called on a progress change event.
*
* @param total The total amount to transmit/receive in bytes.
* @param progress The amount that has been transmitted/received in bytes thus far
*/
fun onAttachmentProgress(total: Long, progress: Long)
}*/
companion object {
@JvmStatic
fun newStreamBuilder(): Builder {
return Builder()
}
}
}
// matches values in AttachmentDatabase.java
enum class AttachmentState(val value: Int) {
DONE(0),
STARTED(1),
PENDING(2),
FAILED(3)
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsession.messaging.sending_receiving.attachments
import org.session.libsignal.libsignal.util.guava.Optional
/**
* Represents a received SignalServiceAttachment "handle." This
* is a pointer to the actual attachment content, which needs to be
* retrieved using [SignalServiceMessageReceiver.retrieveAttachment]
*
* @author Moxie Marlinspike
*/
class SessionServiceAttachmentPointer(val id: Long, contentType: String?, key: ByteArray?,
val size: Optional<Int>, val preview: Optional<ByteArray>,
val width: Int, val height: Int,
val digest: Optional<ByteArray>, val fileName: Optional<String>,
val voiceNote: Boolean, val caption: Optional<String>, url: String) : SessionServiceAttachment(contentType) {
override fun isStream(): Boolean {
return false
}
override fun isPointer(): Boolean {
return true
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.session.libsession.messaging.sending_receiving.attachments
import android.util.Size
import com.google.protobuf.ByteString
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.api.messages.SignalServiceAttachment as SAttachment
import java.io.InputStream
import kotlin.math.round
/**
* Represents a local SignalServiceAttachment to be sent.
*/
class SessionServiceAttachmentStream(val inputStream: InputStream?, contentType: String?, val length: Long, val fileName: Optional<String?>?, val voiceNote: Boolean, val preview: Optional<ByteArray?>, val width: Int, val height: Int, val caption: Optional<String?>, val listener: SAttachment.ProgressListener?) : SessionServiceAttachment(contentType) {
constructor(inputStream: InputStream?, contentType: String?, length: Long, fileName: Optional<String?>?, voiceNote: Boolean, listener: SAttachment.ProgressListener?) : this(inputStream, contentType, length, fileName, voiceNote, Optional.absent<ByteArray?>(), 0, 0, Optional.absent<String?>(), listener) {}
// Though now required, `digest` may be null for pre-existing records or from
// messages received from other clients
var digest: ByteArray? = null
// This only applies for attachments being uploaded.
var isUploaded: Boolean = false
override fun isStream(): Boolean {
return true
}
override fun isPointer(): Boolean {
return false
}
fun toProto(): SignalServiceProtos.AttachmentPointer? {
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
builder.contentType = this.contentType
if (!this.fileName?.get().isNullOrEmpty()) {
builder.fileName = this.fileName?.get()
}
if (!this.caption.get().isNullOrEmpty()) {
builder.caption = this.caption.get()
}
builder.size = this.length.toInt()
builder.key = this.key
builder.digest = ByteString.copyFrom(this.digest)
builder.flags = if (this.voiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
if (this.shouldHaveImageSize()) {
if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) {
val imageSize= Size(this.width, this.height)
val imageWidth = round(imageSize.width.toDouble())
val imageHeight = round(imageSize.height.toDouble())
if (imageWidth > 0 && imageHeight > 0) {
builder.width = imageWidth.toInt()
builder.height = imageHeight.toInt()
}
}
}
builder.url = this.url
try {
return builder.build()
} catch (e: Exception) {
return null
}
}
}