From eb2eecbd85344893fea3191273f5d4fad7b4e3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Sk=C3=A1la?= Date: Sun, 7 Jun 2020 15:59:37 +0200 Subject: [PATCH] Add message attachments --- app/build.gradle | 4 +- .../trustchain/app/TrustChainApplication.kt | 2 +- build.gradle | 2 +- common/build.gradle | 4 +- .../trustchain/common/MarketCommunityTest.kt | 15 +- kotlin-ipv8 | 2 +- peerchat/build.gradle | 6 +- .../peerchat/community/AttachmentPayload.kt | 30 +++ .../community/AttachmentRequestPayload.kt | 23 +++ .../peerchat/community/MessagePayload.kt | 26 ++- .../peerchat/community/PeerChatCommunity.kt | 171 ++++++++++++++++-- .../trustchain/peerchat/db/PeerChatStore.kt | 32 +++- .../trustchain/peerchat/entity/ChatMessage.kt | 15 +- .../conversation/ChatMessageItemRenderer.kt | 26 ++- .../ui/conversation/ConversationFragment.kt | 59 +++++- .../ui/conversation/MessageAttachment.kt | 56 ++++++ .../peerchat/ui/conversation/RichEditText.kt | 29 +++ .../trustchain/peerchat/util/FileUtils.kt | 23 +++ .../ic_baseline_add_photo_alternate_24.xml | 10 + .../main/res/layout/fragment_conversation.xml | 22 ++- peerchat/src/main/res/layout/item_message.xml | 43 +++-- peerchat/src/main/sqldelight/1.sqm | 4 + .../tudelft/peerchat/sqldelight/DbMessage.sq | 14 +- trustchain-explorer/build.gradle | 4 +- 24 files changed, 551 insertions(+), 71 deletions(-) create mode 100644 peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentPayload.kt create mode 100644 peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentRequestPayload.kt create mode 100644 peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/MessageAttachment.kt create mode 100644 peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/RichEditText.kt create mode 100644 peerchat/src/main/java/nl/tudelft/trustchain/peerchat/util/FileUtils.kt create mode 100644 peerchat/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml create mode 100644 peerchat/src/main/sqldelight/1.sqm diff --git a/app/build.gradle b/app/build.gradle index c78d6e330..3ffc33335 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,8 +71,8 @@ android { allWarningsAsErrors = true } - viewBinding { - enabled = true + buildFeatures { + viewBinding = true } packagingOptions { diff --git a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt index aa4690461..a4ba68553 100644 --- a/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt +++ b/app/src/main/java/nl/tudelft/trustchain/app/TrustChainApplication.kt @@ -141,7 +141,7 @@ class TrustChainApplication : Application() { val randomWalk = RandomWalk.Factory() val store = PeerChatStore.getInstance(this) return OverlayConfiguration( - PeerChatCommunity.Factory(store), + PeerChatCommunity.Factory(store, this), listOf(randomWalk) ) } diff --git a/build.gradle b/build.gradle index 3d82e2312..af4384b23 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:4.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jlleitschuh.gradle:ktlint-gradle:$ktlint_gradle_version" classpath "com.squareup.sqldelight:gradle-plugin:$sqldelight_version" diff --git a/common/build.gradle b/common/build.gradle index da4116025..5756377e6 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -38,8 +38,8 @@ android { allWarningsAsErrors = true } - viewBinding { - enabled = true + buildFeatures { + viewBinding = true } testOptions { diff --git a/common/src/test/java/nl/tudelft/trustchain/common/MarketCommunityTest.kt b/common/src/test/java/nl/tudelft/trustchain/common/MarketCommunityTest.kt index f797ec740..b145d5797 100644 --- a/common/src/test/java/nl/tudelft/trustchain/common/MarketCommunityTest.kt +++ b/common/src/test/java/nl/tudelft/trustchain/common/MarketCommunityTest.kt @@ -3,7 +3,6 @@ package nl.tudelft.trustchain.common import io.mockk.* import nl.tudelft.ipv8.Peer import nl.tudelft.ipv8.messaging.EndpointAggregator -import nl.tudelft.ipv8.messaging.Serializable import nl.tudelft.ipv8.peerdiscovery.Network import nl.tudelft.trustchain.common.messaging.TradePayload import org.junit.Assert.assertEquals @@ -20,12 +19,14 @@ class MarketCommunityTest { fun broadcast_callsSendOneTimePerPeer() { val payload = mockk() every { - marketCommunity["serializePacket"]( - any(), - any(), - any(), - any(), - any() + marketCommunity.serializePacket( + any(), + any(), + any(), + any(), + any(), + any(), + any() ) } returns byteArrayOf(0x00) every { marketCommunity.getPeers() } returns getFakePeers() diff --git a/kotlin-ipv8 b/kotlin-ipv8 index 682077b88..fbe551ac4 160000 --- a/kotlin-ipv8 +++ b/kotlin-ipv8 @@ -1 +1 @@ -Subproject commit 682077b88940c0b7109f8ea76cf2290daf6bc19d +Subproject commit fbe551ac4db96a47ddf69f8222fffbd2cc27204a diff --git a/peerchat/build.gradle b/peerchat/build.gradle index 2669df200..93f174c61 100644 --- a/peerchat/build.gradle +++ b/peerchat/build.gradle @@ -48,8 +48,8 @@ android { allWarningsAsErrors = true } - viewBinding { - enabled = true + buildFeatures { + viewBinding = true } } @@ -85,6 +85,8 @@ dependencies { implementation 'com.github.tony19:logback-android:2.0.0' implementation 'com.mattskala:itemadapter:0.3' + implementation 'com.github.bumptech.glide:glide:4.11.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' // Testing testImplementation 'junit:junit:4.12' diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentPayload.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentPayload.kt new file mode 100644 index 000000000..ac7899af0 --- /dev/null +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentPayload.kt @@ -0,0 +1,30 @@ +package nl.tudelft.trustchain.peerchat.community + +import nl.tudelft.ipv8.messaging.* + +class AttachmentPayload( + val id: String, + val data: ByteArray +) : Serializable { + override fun serialize(): ByteArray { + return serializeVarLen(id.toByteArray()) + + serializeVarLen(data) + } + + companion object Deserializer : Deserializable { + override fun deserialize(buffer: ByteArray, offset: Int): Pair { + var localOffset = offset + val (id, idSize) = deserializeVarLen(buffer, localOffset) + localOffset += idSize + val (data, dataSize) = deserializeVarLen(buffer, localOffset) + localOffset += dataSize + return Pair( + AttachmentPayload( + id.toString(Charsets.UTF_8), + data + ), + localOffset - offset + ) + } + } +} diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentRequestPayload.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentRequestPayload.kt new file mode 100644 index 000000000..6de77f0fd --- /dev/null +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/AttachmentRequestPayload.kt @@ -0,0 +1,23 @@ +package nl.tudelft.trustchain.peerchat.community + +import nl.tudelft.ipv8.messaging.* + +data class AttachmentRequestPayload( + val id: String +) : Serializable { + override fun serialize(): ByteArray { + return serializeVarLen(id.toByteArray()) + } + + companion object Deserializer : Deserializable { + override fun deserialize(buffer: ByteArray, offset: Int): Pair { + var localOffset = offset + val (id, idSize) = deserializeVarLen(buffer, localOffset) + localOffset += idSize + return Pair( + AttachmentRequestPayload(id.toString(Charsets.UTF_8)), + localOffset - offset + ) + } + } +} diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/MessagePayload.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/MessagePayload.kt index 38a131cae..992f4be46 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/MessagePayload.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/MessagePayload.kt @@ -2,13 +2,19 @@ package nl.tudelft.trustchain.peerchat.community import nl.tudelft.ipv8.messaging.* -data class MessagePayload( +class MessagePayload constructor( val id: String, - val message: String + val message: String, + val attachmentType: String, + val attachmentSize: Long, + val attachmentContent: ByteArray ) : Serializable { override fun serialize(): ByteArray { return serializeVarLen(id.toByteArray()) + - serializeVarLen(message.toByteArray()) + serializeVarLen(message.toByteArray()) + + serializeVarLen(attachmentType.toByteArray()) + + serializeLong(attachmentSize) + + serializeVarLen(attachmentContent) } companion object Deserializer : Deserializable { @@ -18,8 +24,20 @@ data class MessagePayload( localOffset += idSize val (message, messageSize) = deserializeVarLen(buffer, localOffset) localOffset += messageSize + val (attachmentType, attachmentTypeSize) = deserializeVarLen(buffer, localOffset) + localOffset += attachmentTypeSize + val attachmentSize = deserializeLong(buffer, localOffset) + localOffset += SERIALIZED_LONG_SIZE + val (attachmentContent, attachmentContentSize) = deserializeVarLen(buffer, localOffset) + localOffset += attachmentContentSize return Pair( - MessagePayload(id.toString(Charsets.UTF_8), message.toString(Charsets.UTF_8)), + MessagePayload( + id.toString(Charsets.UTF_8), + message.toString(Charsets.UTF_8), + attachmentType.toString(Charsets.UTF_8), + attachmentSize, + attachmentContent + ), localOffset - offset ) } diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/PeerChatCommunity.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/PeerChatCommunity.kt index 4dd056455..35948906b 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/PeerChatCommunity.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/community/PeerChatCommunity.kt @@ -1,5 +1,6 @@ package nl.tudelft.trustchain.peerchat.community +import android.content.Context import android.database.sqlite.SQLiteConstraintException import android.util.Log import kotlinx.coroutines.delay @@ -9,23 +10,31 @@ import mu.KotlinLogging import nl.tudelft.ipv8.Community import nl.tudelft.ipv8.Overlay import nl.tudelft.ipv8.Peer +import nl.tudelft.ipv8.keyvault.PrivateKey import nl.tudelft.ipv8.keyvault.PublicKey import nl.tudelft.ipv8.messaging.Packet +import nl.tudelft.ipv8.util.hexToBytes import nl.tudelft.ipv8.util.toHex import nl.tudelft.trustchain.peerchat.db.PeerChatStore import nl.tudelft.trustchain.peerchat.entity.ChatMessage +import nl.tudelft.trustchain.peerchat.ui.conversation.MessageAttachment +import java.io.File +import java.io.FileOutputStream import java.util.* private val logger = KotlinLogging.logger {} class PeerChatCommunity( - private val database: PeerChatStore + private val database: PeerChatStore, + private val context: Context ) : Community() { override val serviceId = "ac9c01202e8d01e5f7d3cec88085dd842267c273" init { messageHandlers[MessageId.MESSAGE] = ::onMessagePacket messageHandlers[MessageId.ACK] = ::onAckPacket + messageHandlers[MessageId.ATTACHMENT_REQUEST] = ::onAttachmentRequestPacket + messageHandlers[MessageId.ATTACHMENT] = ::onAttachmentPacket } override fun load() { @@ -33,31 +42,69 @@ class PeerChatCommunity( scope.launch { while (isActive) { - val messages = database.getUnacknowledgedMessages() - Log.d("PeerChat", "Found ${messages.size} outgoing unack messages") - messages.forEach { message -> - sendMessage(message) + try { + // Send unacknowledged messages + val messages = database.getUnacknowledgedMessages() + Log.d("PeerChat", "Found ${messages.size} outgoing unack messages") + messages.forEach { message -> + sendMessage(message) + } + + // Request missing attachments + val attachments = database.getUnfetchedAttachments() + Log.d("PeerChat", "Found ${attachments.size} unfetched attachments") + attachments.forEach { message -> + val mid = message.sender.keyToHash().toHex() + val peer = getPeers().find { it.mid == mid } + if (peer != null && message.attachment != null) { + sendAttachmentRequest(peer, message.attachment.content.toHex()) + } + } + } catch (e: Exception) { + e.printStackTrace() } - delay(10000L) + delay(30000L) } } } fun sendMessage(message: String, recipient: PublicKey) { - val chatMessage = createOutgoingChatMessage(message, recipient) + val chatMessage = createOutgoingChatMessage(message, null, recipient) database.addMessage(chatMessage) sendMessage(chatMessage) } - private fun sendMessage(chatMessage: ChatMessage) { - val payload = MessagePayload(chatMessage.id, chatMessage.message) - // TODO: encrypt - val packet = serializePacket(MessageId.MESSAGE, payload, true) + fun sendImage(file: File, recipient: PublicKey) { + val hash = file.name.hexToBytes() + val attachment = MessageAttachment(MessageAttachment.TYPE_IMAGE, file.length(), hash) + val chatMessage = createOutgoingChatMessage("", attachment, recipient) + database.addMessage(chatMessage) + + sendMessage(chatMessage) + } + private fun sendMessage(chatMessage: ChatMessage) { val mid = chatMessage.recipient.keyToHash().toHex() val peer = getPeers().find { it.mid == mid } + if (peer != null) { + val payload = MessagePayload( + chatMessage.id, + chatMessage.message, + chatMessage.attachment?.type ?: "", + chatMessage.attachment?.size ?: 0L, + chatMessage.attachment?.content ?: ByteArray(0) + ) + + val packet = serializePacket( + MessageId.MESSAGE, + payload, + sign = true, + encrypt = true, + recipient = peer + ) + logger.debug { "-> $payload" } send(peer, packet) } else { @@ -72,8 +119,23 @@ class PeerChatCommunity( send(peer, packet) } + private fun sendAttachmentRequest(peer: Peer, id: String) { + val payload = AttachmentRequestPayload(id) + val packet = serializePacket(MessageId.ATTACHMENT_REQUEST, payload) + logger.debug { "-> $payload" } + send(peer, packet) + } + + private fun sendAttachment(peer: Peer, id: String, file: File) { + val payload = AttachmentPayload(id, file.readBytes()) + val packet = serializePacket(MessageId.ATTACHMENT, payload, encrypt = true, recipient = peer) + logger.debug { "-> $payload" } + send(peer, packet) + } + private fun onMessagePacket(packet: Packet) { - val (peer, payload) = packet.getAuthPayload(MessagePayload.Deserializer) + val (peer, payload) = packet.getDecryptedAuthPayload( + MessagePayload.Deserializer, myPeer.key as PrivateKey) logger.debug { "<- $payload" } onMessage(peer, payload) } @@ -84,6 +146,19 @@ class PeerChatCommunity( onAck(peer, payload) } + private fun onAttachmentRequestPacket(packet: Packet) { + val (peer, payload) = packet.getAuthPayload(AttachmentRequestPayload.Deserializer) + logger.debug { "<- $payload" } + onAttachmentRequest(peer, payload) + } + + private fun onAttachmentPacket(packet: Packet) { + val (_, payload) = packet.getDecryptedAuthPayload( + AttachmentPayload.Deserializer, myPeer.key as PrivateKey) + logger.debug { "<- $payload" } + onAttachment(payload) + } + private fun onMessage(peer: Peer, payload: MessagePayload) { Log.d("PeerChat", "onMessage from $peer: $payload") @@ -96,6 +171,11 @@ class PeerChatCommunity( Log.d("PeerChat", "Sending ack to ${chatMessage.id}") sendAck(peer, chatMessage.id) + + // Request attachment + if (chatMessage.attachment != null) { + sendAttachmentRequest(peer, chatMessage.attachment.content.toHex()) + } } private fun onAck(peer: Peer, payload: AckPayload) { @@ -108,43 +188,100 @@ class PeerChatCommunity( } } - private fun createOutgoingChatMessage(message: String, recipient: PublicKey): ChatMessage { + private fun onAttachmentRequest(peer: Peer, payload: AttachmentRequestPayload) { + try { + val file = MessageAttachment.getFile(context, payload.id) + if (file.exists()) { + sendAttachment(peer, payload.id, file) + } else { + Log.w("PeerChat", "The requested attachment does not exist") + } + } catch (e: SQLiteConstraintException) { + e.printStackTrace() + } + } + + private fun onAttachment(payload: AttachmentPayload) { + try { + val file = MessageAttachment.getFile(context, payload.id) + if (!file.exists()) { + // Save attachment + val os = FileOutputStream(file) + os.write(payload.data) + } + // Mark attachment as fetched + database.setAttachmentFetched(payload.id) + } catch (e: SQLiteConstraintException) { + e.printStackTrace() + } + } + + private fun createOutgoingChatMessage( + message: String, + attachment: MessageAttachment?, + recipient: PublicKey + ): ChatMessage { val id = UUID.randomUUID().toString() return ChatMessage( id, message, + attachment, myPeer.publicKey, recipient, true, Date(), ack = false, - read = true + read = true, + attachmentFetched = true ) } private fun createIncomingChatMessage(peer: Peer, message: MessagePayload): ChatMessage { + /* + // Store attachment + val file = if (message.attachmentData.isNotEmpty()) + saveFile(context, message.attachmentData) else null + */ + return ChatMessage( message.id, message.message, + createMessageAttachment(message), peer.publicKey, myPeer.publicKey, false, Date(), ack = false, - read = false + read = false, + attachmentFetched = false ) } + private fun createMessageAttachment(message: MessagePayload): MessageAttachment? { + return if (message.attachmentType.isNotEmpty()) { + MessageAttachment( + message.attachmentType, + message.attachmentSize, + message.attachmentContent + ) + } else { + null + } + } + object MessageId { const val MESSAGE = 1 const val ACK = 2 + const val ATTACHMENT_REQUEST = 3 + const val ATTACHMENT = 4 } class Factory( - private val database: PeerChatStore + private val database: PeerChatStore, + private val context: Context ) : Overlay.Factory(PeerChatCommunity::class.java) { override fun create(): PeerChatCommunity { - return PeerChatCommunity(database) + return PeerChatCommunity(database, context) } } } diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/db/PeerChatStore.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/db/PeerChatStore.kt index 754ad1a1f..8bb542cc4 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/db/PeerChatStore.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/db/PeerChatStore.kt @@ -1,7 +1,6 @@ package nl.tudelft.trustchain.peerchat.db import android.content.Context -import android.util.Log import com.squareup.sqldelight.android.AndroidSqliteDriver import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList @@ -10,9 +9,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import nl.tudelft.ipv8.keyvault.PublicKey import nl.tudelft.ipv8.keyvault.defaultCryptoProvider +import nl.tudelft.ipv8.util.hexToBytes import nl.tudelft.peerchat.sqldelight.Database import nl.tudelft.trustchain.peerchat.entity.ChatMessage import nl.tudelft.trustchain.peerchat.entity.Contact +import nl.tudelft.trustchain.peerchat.ui.conversation.MessageAttachment import java.util.* class PeerChatStore(context: Context) { @@ -27,21 +28,30 @@ class PeerChatStore(context: Context) { outgoing: Long, timestamp: Long, ack: Long, - read: Long -> + read: Long, + attachmentType: String?, + attachmentSize: Long?, + attachmentContent: ByteArray?, + attachmentFetched: Long -> ChatMessage( id, message, + if (attachmentType != null && attachmentSize != null && attachmentContent != null) + MessageAttachment(attachmentType, + attachmentSize, + attachmentContent + ) else null, defaultCryptoProvider.keyFromPublicBin(senderPk), defaultCryptoProvider.keyFromPublicBin(receipientPk), outgoing == 1L, Date(timestamp), ack == 1L, - read == 1L + read == 1L, + attachmentFetched == 1L ) } fun addContact(publicKey: PublicKey, name: String) { - Log.d("AddContact", "save contact $name $publicKey") database.dbContactQueries.addContact(name, publicKey.keyToBin()) } @@ -61,6 +71,14 @@ class PeerChatStore(context: Context) { return database.dbMessageQueries.getUnacknowledgedMessages(messageMapper).executeAsList() } + fun getUnfetchedAttachments(): List { + return database.dbMessageQueries.getUnfetchedAttachments(messageMapper).executeAsList() + } + + fun setAttachmentFetched(id: String) { + database.dbMessageQueries.setAttachmentFetched(id.hexToBytes()) + } + fun getContactsWithLastMessages(): Flow>> { return combine(getContacts(), getAllMessages()) { contacts, messages -> val notContacts = messages @@ -89,7 +107,11 @@ class PeerChatStore(context: Context) { if (message.outgoing) 1L else 0L, message.timestamp.time, if (message.ack) 1L else 0L, - if (message.read) 1L else 0L + if (message.read) 1L else 0L, + message.attachment?.type, + message.attachment?.size, + message.attachment?.content, + if (message.attachmentFetched) 1L else 0L ) } diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/entity/ChatMessage.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/entity/ChatMessage.kt index e089d6fd6..0ef518158 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/entity/ChatMessage.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/entity/ChatMessage.kt @@ -1,6 +1,7 @@ package nl.tudelft.trustchain.peerchat.entity import nl.tudelft.ipv8.keyvault.PublicKey +import nl.tudelft.trustchain.peerchat.ui.conversation.MessageAttachment import java.util.* data class ChatMessage( @@ -14,6 +15,11 @@ data class ChatMessage( */ val message: String, + /** + * An optional message attachment. + */ + val attachment: MessageAttachment?, + /** * The public key of the message sender. */ @@ -40,7 +46,12 @@ data class ChatMessage( val ack: Boolean, /** - * True if the message has been read. + * True if the message has been read (for incoming messages). + */ + val read: Boolean, + + /** + * True if the attachment has been fetched and stored locally (for incoming messages). */ - val read: Boolean + val attachmentFetched: Boolean ) diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ChatMessageItemRenderer.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ChatMessageItemRenderer.kt index 556d88dcf..d307ca16f 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ChatMessageItemRenderer.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ChatMessageItemRenderer.kt @@ -6,6 +6,7 @@ import android.text.format.DateUtils import android.view.View import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible +import com.bumptech.glide.Glide import com.mattskala.itemadapter.ItemLayoutRenderer import kotlinx.android.synthetic.main.item_message.view.* import nl.tudelft.trustchain.common.util.getColorByHash @@ -32,11 +33,30 @@ class ChatMessageItemRenderer : ItemLayoutRenderer( avatar.isVisible = item.shouldShowAvatar imgDelivered.isVisible = item.chatMessage.outgoing && item.chatMessage.ack constraintSet.clone(constraintLayout) - constraintSet.removeFromHorizontalChain(txtMessage.id) + constraintSet.removeFromHorizontalChain(content.id) constraintSet.removeFromHorizontalChain(bottomContainer.id) + + val attachment = item.chatMessage.attachment + if (attachment != null && attachment.type == MessageAttachment.TYPE_IMAGE) { + val file = attachment.getFile(view.context) + if (file.exists()) { + Glide.with(view).load(file).into(image) + progress.isVisible = false + } else { + image.setImageBitmap(null) + progress.isVisible = true + } + image.isVisible = true + txtMessage.isVisible = false + } else { + image.isVisible = false + txtMessage.isVisible = true + progress.isVisible = false + } + if (item.chatMessage.outgoing) { constraintSet.connect( - txtMessage.id, + content.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END @@ -51,7 +71,7 @@ class ChatMessageItemRenderer : ItemLayoutRenderer( val avatarMargin = resources.getDimensionPixelSize(R.dimen.avatar_size) + resources.getDimensionPixelSize(R.dimen.avatar_margin) constraintSet.connect( - txtMessage.id, + content.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ConversationFragment.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ConversationFragment.kt index 15ef06b6b..50f9e55ae 100644 --- a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ConversationFragment.kt +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/ConversationFragment.kt @@ -1,14 +1,20 @@ package nl.tudelft.trustchain.peerchat.ui.conversation +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.View -import androidx.lifecycle.* +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.asLiveData import androidx.recyclerview.widget.LinearLayoutManager import com.mattskala.itemadapter.Item import com.mattskala.itemadapter.ItemAdapter -import kotlinx.android.synthetic.main.fragment_add_contact.* import kotlinx.android.synthetic.main.fragment_conversation.* -import kotlinx.coroutines.* import kotlinx.coroutines.flow.map import nl.tudelft.ipv8.keyvault.defaultCryptoProvider import nl.tudelft.ipv8.util.hexToBytes @@ -19,6 +25,7 @@ import nl.tudelft.trustchain.peerchat.community.PeerChatCommunity import nl.tudelft.trustchain.peerchat.databinding.FragmentConversationBinding import nl.tudelft.trustchain.peerchat.db.PeerChatStore import nl.tudelft.trustchain.peerchat.entity.ChatMessage +import nl.tudelft.trustchain.peerchat.util.saveFile class ConversationFragment : BaseFragment(R.layout.fragment_conversation) { private val binding by viewBinding(FragmentConversationBinding::bind) @@ -44,6 +51,25 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation) { requireArguments().getString(ARG_NAME)!! } + private val onCommitContentListener = InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ -> + val lacksPermission = (flags and + InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) { + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + return@OnCommitContentListener false // return false if failed + } + } + + val uri = inputContentInfo.contentUri + Log.d("ConversationFragment", "uri: $uri") + + sendImageFromUri(uri) + + true + } + private fun getPeerChatCommunity(): PeerChatCommunity { return getIpv8().getOverlay() ?: throw java.lang.IllegalStateException("PeerChatCommunity is not configured") } @@ -69,6 +95,8 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation) { binding.recyclerView.adapter = adapter binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.edtMessage.onCommitContentListener = onCommitContentListener + btnSend.setOnClickListener { val message = binding.edtMessage.text.toString() if (message.isNotEmpty()) { @@ -76,6 +104,25 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation) { binding.edtMessage.text = null } } + + btnAddImage.setOnClickListener { + val intent = Intent() + intent.type = "image/*" + intent.action = Intent.ACTION_GET_CONTENT + startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_IMAGE) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + PICK_IMAGE -> if (resultCode == Activity.RESULT_OK && data != null) { + val uri = data.data + if (uri != null) { + sendImageFromUri(uri) + } + } + else -> super.onActivityResult(requestCode, resultCode, data) + } } private fun createItems(messages: List): List { @@ -93,10 +140,16 @@ class ConversationFragment : BaseFragment(R.layout.fragment_conversation) { } } + private fun sendImageFromUri(uri: Uri) { + val file = saveFile(requireContext(), uri) + getPeerChatCommunity().sendImage(file, publicKey) + } + companion object { const val ARG_PUBLIC_KEY = "public_key" const val ARG_NAME = "name" private const val GROUP_TIME_LIMIT = 60 * 1000 + private const val PICK_IMAGE = 10 } } diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/MessageAttachment.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/MessageAttachment.kt new file mode 100644 index 000000000..da7b5d605 --- /dev/null +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/MessageAttachment.kt @@ -0,0 +1,56 @@ +package nl.tudelft.trustchain.peerchat.ui.conversation + +import android.content.Context +import nl.tudelft.ipv8.util.toHex +import java.io.File + +@OptIn(ExperimentalUnsignedTypes::class) +data class MessageAttachment constructor( + /** + * The type of the attachment. Currently, only "image" is supported. + */ + val type: String, + + /** + * The size of the attachment in bytes. + */ + val size: Long, + + /** + * The hash of the attachment that can be used for retrieving its data. + */ + val content: ByteArray +) { + fun getFile(context: Context): File { + return getFile(context, content.toHex()) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageAttachment + + if (type != other.type) return false + if (size != other.size) return false + if (!content.contentEquals(other.content)) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + content.contentHashCode() + return result + } + + companion object { + const val TYPE_IMAGE = "image" + + fun getFile(context: Context, id: String): File { + val path = "" + context.filesDir + "/" + id + return File(path) + } + } +} diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/RichEditText.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/RichEditText.kt new file mode 100644 index 000000000..af5a52c6a --- /dev/null +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/ui/conversation/RichEditText.kt @@ -0,0 +1,29 @@ +package nl.tudelft.trustchain.peerchat.ui.conversation + +import android.content.Context +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.appcompat.R +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat + +class RichEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.editTextStyle +) : androidx.appcompat.widget.AppCompatEditText(context, attrs, defStyleAttr) { + var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { + val ic: InputConnection = super.onCreateInputConnection(editorInfo) + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) + + val callback = InputConnectionCompat.OnCommitContentListener { + inputContentInfo, flags, opts -> + onCommitContentListener?.onCommitContent(inputContentInfo, flags, opts) ?: false + } + + return InputConnectionCompat.createWrapper(ic, editorInfo, callback) + } +} diff --git a/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/util/FileUtils.kt b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/util/FileUtils.kt new file mode 100644 index 000000000..bc118eedc --- /dev/null +++ b/peerchat/src/main/java/nl/tudelft/trustchain/peerchat/util/FileUtils.kt @@ -0,0 +1,23 @@ +package nl.tudelft.trustchain.peerchat.util + +import android.content.Context +import android.net.Uri +import nl.tudelft.ipv8.util.sha256 +import nl.tudelft.ipv8.util.toHex +import nl.tudelft.trustchain.peerchat.ui.conversation.MessageAttachment +import java.io.* + +fun saveFile(context: Context, uri: Uri): File { + val inputStream = context.contentResolver.openInputStream(uri) + val bytes = inputStream!!.readBytes() + return saveFile(context, bytes) +} + +fun saveFile(context: Context, bytes: ByteArray): File { + val id = sha256(bytes).toHex() + val file = MessageAttachment.getFile(context, id) + val outputStream = FileOutputStream(file) + outputStream.write(bytes) + outputStream.close() + return file +} diff --git a/peerchat/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml b/peerchat/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml new file mode 100644 index 000000000..5044c52be --- /dev/null +++ b/peerchat/src/main/res/drawable/ic_baseline_add_photo_alternate_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/peerchat/src/main/res/layout/fragment_conversation.xml b/peerchat/src/main/res/layout/fragment_conversation.xml index d818290c7..0462a5c7e 100644 --- a/peerchat/src/main/res/layout/fragment_conversation.xml +++ b/peerchat/src/main/res/layout/fragment_conversation.xml @@ -19,17 +19,31 @@ android:layout_height="wrap_content" android:padding="8dp"> - + + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/btnAddImage" + app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintStart_toStartOf="parent"> + + + + + + + + app:layout_constraintTop_toBottomOf="@id/content" + app:layout_constraintStart_toStartOf="@id/content">